~bzoltan/click/transition_mirrors

« back to all changes in this revision

Viewing changes to tests/integration/test_signatures.py

  • Committer: CI bot
  • Author(s): Michael Vogt, Colin Watson
  • Date: 2014-08-22 17:18:56 UTC
  • mfrom: (425.1.79 devel)
  • Revision ID: ps-jenkins@lists.canonical.com-20140822171856-06cj8hcsjbfrybex
Click 0.4.31: "click info <file in unpacked package>", and basic support for package signing. Fixes: 1324853, 1330770

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
import unittest
 
24
from textwrap import dedent
 
25
 
 
26
from .helpers import (
 
27
    is_root,
 
28
    ClickTestCase,
 
29
)
 
30
 
 
31
def makedirs(path):
 
32
    try:
 
33
        os.makedirs(path)
 
34
    except OSError:
 
35
        pass
 
36
 
 
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:"):
 
44
            continue
 
45
        return line.split(":")[4]
 
46
    raise ValueError("Cannot find public key in output: '%s'" % output)
 
47
 
 
48
 
 
49
class Debsigs:
 
50
    """Tiny wrapper around the debsigs CLI"""
 
51
    def __init__(self, gpghome, keyid):
 
52
        self.keyid = keyid
 
53
        self.gpghome = gpghome
 
54
        self.policy = "/etc/debsig/policies/%s/generic.pol" % self.keyid
 
55
 
 
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(
 
61
            ["debsigs",
 
62
             "--sign=%s" % signature_type,
 
63
             "--default-key=%s" % self.keyid,
 
64
             filepath], env=env)
 
65
 
 
66
    def install_signature_policy(self):
 
67
        """Install/update the system-wide signature policy"""
 
68
        xmls = dedent("""\
 
69
        <?xml version="1.0"?>
 
70
        <!DOCTYPE Policy SYSTEM "http://www.debian.org/debsig/1.0/policy.dtd">
 
71
        <Policy xmlns="http://www.debian.org/debsig/1.0/">
 
72
 
 
73
        <Origin Name="test-origin" id="{keyid}" Description="Example policy"/>
 
74
        <Selection>
 
75
        <Required Type="origin" File="{filename}" id="{keyid}"/>
 
76
        </Selection>
 
77
  
 
78
        <Verification>
 
79
        <Required Type="origin" File="{filename}" id="{keyid}"/>
 
80
        </Verification>
 
81
        </Policy>
 
82
        """.format(keyid=self.keyid, filename="origin.pub"))
 
83
        makedirs(os.path.dirname(self.policy))
 
84
        with open(self.policy, "w") as f:
 
85
            f.write(xmls)
 
86
        self.pubkey_path = (
 
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)
 
90
 
 
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
 
94
        #        system root
 
95
        os.remove(self.policy)
 
96
        os.remove(self.pubkey_path)
 
97
 
 
98
 
 
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)
 
110
 
 
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)
 
116
            print(output)
 
117
 
 
118
        output = cm.exception.output
 
119
        expected_error_message = "Signature verification error: "
 
120
        self.assertIn(expected_error_message, output)
 
121
 
 
122
 
 
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])
 
129
 
 
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])
 
134
 
 
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,
 
142
             path_to_click])
 
143
        self.addCleanup(
 
144
            subprocess.call, [self.click_binary, "unregister",
 
145
                              "--user=%s" % user, name])
 
146
 
 
147
 
 
148
@unittest.skipIf(not is_root(), "This tests needs to run as root")
 
149
class TestSignatureVerification(ClickSignaturesTestCase):
 
150
    def setUp(self):
 
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()
 
160
 
 
161
    def tearDown(self):
 
162
        self.debsigs.uninstall_signature_policy()
 
163
 
 
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,
 
171
             path_to_click])
 
172
        self.addCleanup(
 
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)
 
179
        
 
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)
 
193
 
 
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")
 
200
        # add a invalid sig
 
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)
 
207
 
 
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"],
 
212
                cwd=self.temp_dir)
 
213
            altered_member = os.path.join(self.temp_dir, member)
 
214
            with open(altered_member, "ba") as f:
 
215
                f.write(b"\0")
 
216
            subprocess.check_call(["ar", "-r", path_to_click, altered_member])
 
217
 
 
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)
 
229
 
 
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:
 
236
            tar.add(evilfile)
 
237
        return new_data_tar
 
238
 
 
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",
 
248
                               "-r", 
 
249
                               "-b", "data.tar.gz",
 
250
                               path_to_click,
 
251
                               new_data])
 
252
        output = subprocess.check_output(
 
253
            ["ar", "-t", path_to_click], universal_newlines=True)
 
254
        self.assertEqual(output.splitlines(),
 
255
                         ["debian-binary",
 
256
                          "_click-binary",
 
257
                          "control.tar.gz",
 
258
                          "data.tar.bz2",
 
259
                          "data.tar.gz",
 
260
                          "_gpgorigin"])
 
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)
 
269
 
 
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",
 
277
                               "-d", 
 
278
                               path_to_click,
 
279
                               "data.tar.gz",
 
280
                               ])
 
281
        subprocess.check_call(["ar",
 
282
                               "-r", 
 
283
                               path_to_click,
 
284
                               new_data])
 
285
        output = subprocess.check_output(
 
286
            ["ar", "-t", path_to_click], universal_newlines=True)
 
287
        self.assertEqual(output.splitlines(),
 
288
                         ["debian-binary",
 
289
                          "_click-binary",
 
290
                          "control.tar.gz",
 
291
                          "_gpgorigin",
 
292
                          "data.tar.bz2",
 
293
                         ])
 
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)
 
303
 
 
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",
 
316
                               "-r", 
 
317
                               path_to_click,
 
318
                               new_data,
 
319
                               ])
 
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)
 
348