~mvo/click/lp1232130-kill-on-remove

« back to all changes in this revision

Viewing changes to click/tests/integration/test_signatures.py

  • Committer: Michael Vogt
  • Date: 2014-09-29 11:12:52 UTC
  • mfrom: (424.1.99 devel)
  • Revision ID: michael.vogt@ubuntu.com-20140929111252-o3vsvp2e4d620h21
mergedĀ lp:click/devel

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2014 Canonical Ltd.
 
2
# Author: Michael Vogt <michael.vogt@ubuntu.com>
 
3
 
 
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.
 
7
#
 
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.
 
12
#
 
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/>.
 
15
 
 
16
"""Integration tests for the click signature checking."""
 
17
 
 
18
import copy
 
19
import os
 
20
import shutil
 
21
import subprocess
 
22
import tarfile
 
23
from textwrap import dedent
 
24
 
 
25
from .helpers import (
 
26
    require_root,
 
27
    ClickTestCase,
 
28
)
 
29
 
 
30
 
 
31
def makedirs(path):
 
32
    try:
 
33
        os.makedirs(path)
 
34
    except OSError:
 
35
        pass
 
36
 
 
37
 
 
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:"):
 
45
            continue
 
46
        return line.split(":")[4]
 
47
    raise ValueError("Cannot find public key in output: '%s'" % output)
 
48
 
 
49
 
 
50
class Debsigs:
 
51
    """Tiny wrapper around the debsigs CLI"""
 
52
    def __init__(self, gpghome, keyid):
 
53
        self.keyid = keyid
 
54
        self.gpghome = gpghome
 
55
        self.policy = "/etc/debsig/policies/%s/generic.pol" % self.keyid
 
56
 
 
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(
 
62
            ["debsigs",
 
63
             "--sign=%s" % signature_type,
 
64
             "--default-key=%s" % self.keyid,
 
65
             filepath], env=env)
 
66
 
 
67
    def install_signature_policy(self):
 
68
        """Install/update the system-wide signature policy"""
 
69
        xmls = dedent("""\
 
70
        <?xml version="1.0"?>
 
71
        <!DOCTYPE Policy SYSTEM "http://www.debian.org/debsig/1.0/policy.dtd">
 
72
        <Policy xmlns="http://www.debian.org/debsig/1.0/">
 
73
 
 
74
        <Origin Name="test-origin" id="{keyid}" Description="Example policy"/>
 
75
        <Selection>
 
76
        <Required Type="origin" File="{filename}" id="{keyid}"/>
 
77
        </Selection>
 
78
 
 
79
        <Verification>
 
80
        <Required Type="origin" File="{filename}" id="{keyid}"/>
 
81
        </Verification>
 
82
        </Policy>
 
83
        """.format(keyid=self.keyid, filename="origin.pub"))
 
84
        makedirs(os.path.dirname(self.policy))
 
85
        with open(self.policy, "w") as f:
 
86
            f.write(xmls)
 
87
        self.pubkey_path = (
 
88
            "/usr/share/debsig/keyrings/%s/origin.pub" % self.keyid)
 
89
        makedirs(os.path.dirname(self.pubkey_path))
 
90
        shutil.copy(
 
91
            os.path.join(self.gpghome, "pubring.gpg"), self.pubkey_path)
 
92
 
 
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
 
96
        #        system root
 
97
        os.remove(self.policy)
 
98
        os.remove(self.pubkey_path)
 
99
 
 
100
 
 
101
class ClickSignaturesTestCase(ClickTestCase):
 
102
 
 
103
    @classmethod
 
104
    def setUpClass(cls):
 
105
        super(ClickSignaturesTestCase, cls).setUpClass()
 
106
        require_root()
 
107
 
 
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)
 
117
 
 
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)
 
123
            print(output)
 
124
 
 
125
        output = cm.exception.output
 
126
        expected_error_message = "Signature verification error: "
 
127
        self.assertIn(expected_error_message, output)
 
128
 
 
129
 
 
130
class TestSignatureVerificationNoSignature(ClickSignaturesTestCase):
 
131
 
 
132
    @classmethod
 
133
    def setUpClass(cls):
 
134
        super(TestSignatureVerificationNoSignature, cls).setUpClass()
 
135
        require_root()
 
136
 
 
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])
 
141
 
 
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])
 
146
 
 
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,
 
154
             path_to_click])
 
155
        self.addCleanup(
 
156
            subprocess.call, [self.click_binary, "unregister",
 
157
                              "--user=%s" % user, name])
 
158
 
 
159
 
 
160
class TestSignatureVerification(ClickSignaturesTestCase):
 
161
 
 
162
    @classmethod
 
163
    def setUpClass(cls):
 
164
        super(TestSignatureVerification, cls).setUpClass()
 
165
        require_root()
 
166
 
 
167
    def setUp(self):
 
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()
 
177
 
 
178
    def tearDown(self):
 
179
        self.debsigs.uninstall_signature_policy()
 
180
 
 
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,
 
188
             path_to_click])
 
189
        self.addCleanup(
 
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)
 
196
 
 
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)
 
210
 
 
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")
 
217
        # add a invalid sig
 
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)
 
224
 
 
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"],
 
229
                cwd=self.temp_dir)
 
230
            altered_member = os.path.join(self.temp_dir, member)
 
231
            with open(altered_member, "ba") as f:
 
232
                f.write(b"\0")
 
233
            subprocess.check_call(["ar", "-r", path_to_click, altered_member])
 
234
 
 
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)
 
246
 
 
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:
 
253
            tar.add(evilfile)
 
254
        return new_data_tar
 
255
 
 
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",
 
265
                               "-r",
 
266
                               "-b", "data.tar.gz",
 
267
                               path_to_click,
 
268
                               new_data])
 
269
        output = subprocess.check_output(
 
270
            ["ar", "-t", path_to_click], universal_newlines=True)
 
271
        self.assertEqual(output.splitlines(),
 
272
                         ["debian-binary",
 
273
                          "_click-binary",
 
274
                          "control.tar.gz",
 
275
                          "data.tar.bz2",
 
276
                          "data.tar.gz",
 
277
                          "_gpgorigin"])
 
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)
 
286
 
 
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",
 
294
                               "-d",
 
295
                               path_to_click,
 
296
                               "data.tar.gz",
 
297
                               ])
 
298
        subprocess.check_call(["ar",
 
299
                               "-r",
 
300
                               path_to_click,
 
301
                               new_data])
 
302
        output = subprocess.check_output(
 
303
            ["ar", "-t", path_to_click], universal_newlines=True)
 
304
        self.assertEqual(output.splitlines(),
 
305
                         ["debian-binary",
 
306
                          "_click-binary",
 
307
                          "control.tar.gz",
 
308
                          "_gpgorigin",
 
309
                          "data.tar.bz2",
 
310
                          ])
 
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)
 
320
 
 
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",
 
333
                               "-r",
 
334
                               path_to_click,
 
335
                               new_data,
 
336
                               ])
 
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)