~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/gpg.py

  • Committer: Jelmer Vernooij
  • Date: 2011-08-19 22:34:02 UTC
  • mto: This revision was merged to the branch mainline in revision 6089.
  • Revision ID: jelmer@samba.org-20110819223402-wjywqb0fa1xxx522
Use get_transport_from_{url,path} in more places.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
 
1
# Copyright (C) 2005, 2011 Canonical Ltd
2
2
#   Authors: Robert Collins <robert.collins@canonical.com>
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
13
13
#
14
14
# You should have received a copy of the GNU General Public License
15
15
# along with this program; if not, write to the Free Software
16
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
"""GPG signing and checking logic."""
19
19
 
20
20
import os
21
21
import sys
 
22
from StringIO import StringIO
22
23
 
23
24
from bzrlib.lazy_import import lazy_import
24
25
lazy_import(globals(), """
32
33
    )
33
34
""")
34
35
 
 
36
class i18n:
 
37
    """this class is ready to use bzrlib.i18n but bzrlib.i18n is not ready to
 
38
    use so here is a stub until it is"""
 
39
    @staticmethod
 
40
    def gettext(string):
 
41
        return string
 
42
        
 
43
    @staticmethod
 
44
    def ngettext(single, plural, number):
 
45
        if number == 1:
 
46
            return single
 
47
        else:
 
48
            return plural
 
49
 
 
50
#verification results
 
51
SIGNATURE_VALID = 0
 
52
SIGNATURE_KEY_MISSING = 1
 
53
SIGNATURE_NOT_VALID = 2
 
54
SIGNATURE_NOT_SIGNED = 3
 
55
 
35
56
 
36
57
class DisabledGPGStrategy(object):
37
58
    """A GPG Strategy that makes everything fail."""
38
59
 
 
60
    @staticmethod
 
61
    def verify_signatures_available():
 
62
        return True
 
63
 
39
64
    def __init__(self, ignored):
40
65
        """Real strategies take a configuration."""
41
66
 
42
67
    def sign(self, content):
43
68
        raise errors.SigningFailed('Signing is disabled.')
44
69
 
 
70
    def verify(self, content, testament):
 
71
        raise errors.SignatureVerificationFailed('Signature verification is \
 
72
disabled.')
 
73
 
 
74
    def set_acceptable_keys(self, command_line_input):
 
75
        pass
 
76
 
45
77
 
46
78
class LoopbackGPGStrategy(object):
47
 
    """A GPG Strategy that acts like 'cat' - data is just passed through."""
 
79
    """A GPG Strategy that acts like 'cat' - data is just passed through.
 
80
    Used in tests.
 
81
    """
 
82
 
 
83
    @staticmethod
 
84
    def verify_signatures_available():
 
85
        return True
48
86
 
49
87
    def __init__(self, ignored):
50
88
        """Real strategies take a configuration."""
51
89
 
52
90
    def sign(self, content):
53
 
        return content
 
91
        return ("-----BEGIN PSEUDO-SIGNED CONTENT-----\n" + content +
 
92
                "-----END PSEUDO-SIGNED CONTENT-----\n")
 
93
 
 
94
    def verify(self, content, testament):
 
95
        return SIGNATURE_VALID, None
 
96
 
 
97
    def set_acceptable_keys(self, command_line_input):
 
98
        if command_line_input is not None:
 
99
            patterns = command_line_input.split(",")
 
100
            self.acceptable_keys = []
 
101
            for pattern in patterns:
 
102
                if pattern == "unknown":
 
103
                    pass
 
104
                else:
 
105
                    self.acceptable_keys.append(pattern)
 
106
 
 
107
    def do_verifications(self, revisions, repository):
 
108
        count = {SIGNATURE_VALID: 0,
 
109
                 SIGNATURE_KEY_MISSING: 0,
 
110
                 SIGNATURE_NOT_VALID: 0,
 
111
                 SIGNATURE_NOT_SIGNED: 0}
 
112
        result = []
 
113
        all_verifiable = True
 
114
        for rev_id in revisions:
 
115
            verification_result, uid =\
 
116
                                repository.verify_revision(rev_id,self)
 
117
            result.append([rev_id, verification_result, uid])
 
118
            count[verification_result] += 1
 
119
            if verification_result != SIGNATURE_VALID:
 
120
                all_verifiable = False
 
121
        return (count, result, all_verifiable)
 
122
 
 
123
    def valid_commits_message(self, count):
 
124
        return i18n.gettext(u"{0} commits with valid signatures").format(
 
125
                                        count[SIGNATURE_VALID])            
 
126
 
 
127
    def unknown_key_message(self, count):
 
128
        return i18n.ngettext(u"{0} commit with unknown key",
 
129
                             u"{0} commits with unknown keys",
 
130
                             count[SIGNATURE_KEY_MISSING]).format(
 
131
                                        count[SIGNATURE_KEY_MISSING])
 
132
 
 
133
    def commit_not_valid_message(self, count):
 
134
        return i18n.ngettext(u"{0} commit not valid",
 
135
                             u"{0} commits not valid",
 
136
                             count[SIGNATURE_NOT_VALID]).format(
 
137
                                            count[SIGNATURE_NOT_VALID])
 
138
 
 
139
    def commit_not_signed_message(self, count):
 
140
        return i18n.ngettext(u"{0} commit not signed",
 
141
                             u"{0} commits not signed",
 
142
                             count[SIGNATURE_NOT_SIGNED]).format(
 
143
                                        count[SIGNATURE_NOT_SIGNED])
54
144
 
55
145
 
56
146
def _set_gpg_tty():
68
158
 
69
159
class GPGStrategy(object):
70
160
    """GPG Signing and checking facilities."""
71
 
        
 
161
 
 
162
    acceptable_keys = None
 
163
 
 
164
    @staticmethod
 
165
    def verify_signatures_available():
 
166
        """
 
167
        check if this strategy can verify signatures
 
168
 
 
169
        :return: boolean if this strategy can verify signatures
 
170
        """
 
171
        try:
 
172
            import gpgme
 
173
            return True
 
174
        except ImportError, error:
 
175
            return False
 
176
 
72
177
    def _command_line(self):
73
 
        return [self._config.gpg_signing_command(), '--clearsign']
 
178
        
 
179
        return [self._config.gpg_signing_command(), '--clearsign', '-u',
 
180
                                                self._config.gpg_signing_key()]
74
181
 
75
182
    def __init__(self, config):
76
183
        self._config = config
 
184
        try:
 
185
            import gpgme
 
186
            self.context = gpgme.Context()
 
187
        except ImportError, error:
 
188
            pass # can't use verify()
77
189
 
78
190
    def sign(self, content):
79
191
        if isinstance(content, unicode):
110
222
                raise errors.SigningFailed(self._command_line())
111
223
            else:
112
224
                raise
 
225
 
 
226
    def verify(self, content, testament):
 
227
        """Check content has a valid signature.
 
228
        
 
229
        :param content: the commit signature
 
230
        :param testament: the valid testament string for the commit
 
231
        
 
232
        :return: SIGNATURE_VALID or a failed SIGNATURE_ value, key uid if valid
 
233
        """
 
234
        try:
 
235
            import gpgme
 
236
        except ImportError, error:
 
237
            raise errors.GpgmeNotInstalled(error)
 
238
 
 
239
        signature = StringIO(content)
 
240
        plain_output = StringIO()
 
241
        
 
242
        try:
 
243
            result = self.context.verify(signature, None, plain_output)
 
244
        except gpgme.GpgmeError,error:
 
245
            raise errors.SignatureVerificationFailed(error[2])
 
246
 
 
247
        if len(result) == 0:
 
248
            return SIGNATURE_NOT_VALID, None
 
249
        fingerprint = result[0].fpr
 
250
        if self.acceptable_keys is not None:
 
251
            if not fingerprint in self.acceptable_keys:
 
252
                return SIGNATURE_KEY_MISSING, fingerprint[-8:]
 
253
        if testament != plain_output.getvalue():
 
254
            return SIGNATURE_NOT_VALID, None
 
255
        if result[0].summary & gpgme.SIGSUM_VALID:
 
256
            key = self.context.get_key(fingerprint)
 
257
            name = key.uids[0].name
 
258
            email = key.uids[0].email
 
259
            return SIGNATURE_VALID, name + " <" + email + ">"
 
260
        if result[0].summary & gpgme.SIGSUM_RED:
 
261
            return SIGNATURE_NOT_VALID, None
 
262
        if result[0].summary & gpgme.SIGSUM_KEY_MISSING:
 
263
            return SIGNATURE_KEY_MISSING, fingerprint[-8:]
 
264
        #summary isn't set if sig is valid but key is untrusted
 
265
        if result[0].summary == 0 and self.acceptable_keys is not None:
 
266
            if fingerprint in self.acceptable_keys:
 
267
                return SIGNATURE_VALID, None
 
268
        else:
 
269
            return SIGNATURE_KEY_MISSING, None
 
270
        raise errors.SignatureVerificationFailed("Unknown GnuPG key "\
 
271
                                                 "verification result")
 
272
 
 
273
    def set_acceptable_keys(self, command_line_input):
 
274
        """sets the acceptable keys for verifying with this GPGStrategy
 
275
        
 
276
        :param command_line_input: comma separated list of patterns from
 
277
                                command line
 
278
        :return: nothing
 
279
        """
 
280
        key_patterns = None
 
281
        acceptable_keys_config = self._config.acceptable_keys()
 
282
        try:
 
283
            if isinstance(acceptable_keys_config, unicode):
 
284
                acceptable_keys_config = str(acceptable_keys_config)
 
285
        except UnicodeEncodeError:
 
286
            #gpg Context.keylist(pattern) does not like unicode
 
287
            raise errors.BzrCommandError('Only ASCII permitted in option names')
 
288
 
 
289
        if acceptable_keys_config is not None:
 
290
            key_patterns = acceptable_keys_config
 
291
        if command_line_input is not None: #command line overrides config
 
292
            key_patterns = command_line_input
 
293
        if key_patterns is not None:
 
294
            patterns = key_patterns.split(",")
 
295
 
 
296
            self.acceptable_keys = []
 
297
            for pattern in patterns:
 
298
                result = self.context.keylist(pattern)
 
299
                found_key = False
 
300
                for key in result:
 
301
                    found_key = True
 
302
                    self.acceptable_keys.append(key.subkeys[0].fpr)
 
303
                    trace.mutter("Added acceptable key: " + key.subkeys[0].fpr)
 
304
                if not found_key:
 
305
                    trace.note(i18n.gettext(
 
306
                            "No GnuPG key results for pattern: {}"
 
307
                                ).format(pattern))
 
308
 
 
309
    def do_verifications(self, revisions, repository,
 
310
                            process_events_callback=None):
 
311
        """do verifications on a set of revisions
 
312
        
 
313
        :param revisions: list of revision ids to verify
 
314
        :param repository: repository object
 
315
        :param process_events_callback: method to call for GUI frontends that
 
316
                                                want to keep their UI refreshed
 
317
        
 
318
        :return: count dictionary of results of each type,
 
319
                 result list for each revision,
 
320
                 boolean True if all results are verified successfully
 
321
        """
 
322
        count = {SIGNATURE_VALID: 0,
 
323
                 SIGNATURE_KEY_MISSING: 0,
 
324
                 SIGNATURE_NOT_VALID: 0,
 
325
                 SIGNATURE_NOT_SIGNED: 0}
 
326
        result = []
 
327
        all_verifiable = True
 
328
        for rev_id in revisions:
 
329
            verification_result, uid =\
 
330
                                repository.verify_revision(rev_id,self)
 
331
            result.append([rev_id, verification_result, uid])
 
332
            count[verification_result] += 1
 
333
            if verification_result != SIGNATURE_VALID:
 
334
                all_verifiable = False
 
335
            if process_events_callback is not None:
 
336
                process_events_callback()
 
337
        return (count, result, all_verifiable)
 
338
 
 
339
    def verbose_valid_message(self, result):
 
340
        """takes a verify result and returns list of signed commits strings"""
 
341
        signers = {}
 
342
        for rev_id, validity, uid in result:
 
343
            if validity == SIGNATURE_VALID:
 
344
                signers.setdefault(uid, 0)
 
345
                signers[uid] += 1
 
346
        result = []
 
347
        for uid, number in signers.items():
 
348
             result.append( i18n.ngettext(u"{0} signed {1} commit", 
 
349
                             u"{0} signed {1} commits",
 
350
                             number).format(uid, number) )
 
351
        return result
 
352
 
 
353
 
 
354
    def verbose_not_valid_message(self, result, repo):
 
355
        """takes a verify result and returns list of not valid commit info"""
 
356
        signers = {}
 
357
        for rev_id, validity, empty in result:
 
358
            if validity == SIGNATURE_NOT_VALID:
 
359
                revision = repo.get_revision(rev_id)
 
360
                authors = ', '.join(revision.get_apparent_authors())
 
361
                signers.setdefault(authors, 0)
 
362
                signers[authors] += 1
 
363
        result = []
 
364
        for authors, number in signers.items():
 
365
            result.append( i18n.ngettext(u"{0} commit by author {1}", 
 
366
                                 u"{0} commits by author {1}",
 
367
                                 number).format(number, authors) )
 
368
        return result
 
369
 
 
370
    def verbose_not_signed_message(self, result, repo):
 
371
        """takes a verify result and returns list of not signed commit info"""
 
372
        signers = {}
 
373
        for rev_id, validity, empty in result:
 
374
            if validity == SIGNATURE_NOT_SIGNED:
 
375
                revision = repo.get_revision(rev_id)
 
376
                authors = ', '.join(revision.get_apparent_authors())
 
377
                signers.setdefault(authors, 0)
 
378
                signers[authors] += 1
 
379
        result = []
 
380
        for authors, number in signers.items():
 
381
            result.append( i18n.ngettext(u"{0} commit by author {1}", 
 
382
                                 u"{0} commits by author {1}",
 
383
                                 number).format(number, authors) )
 
384
        return result
 
385
 
 
386
    def verbose_missing_key_message(self, result):
 
387
        """takes a verify result and returns list of missing key info"""
 
388
        signers = {}
 
389
        for rev_id, validity, fingerprint in result:
 
390
            if validity == SIGNATURE_KEY_MISSING:
 
391
                signers.setdefault(fingerprint, 0)
 
392
                signers[fingerprint] += 1
 
393
        result = []
 
394
        for fingerprint, number in signers.items():
 
395
            result.append( i18n.ngettext(u"Unknown key {0} signed {1} commit", 
 
396
                                 u"Unknown key {0} signed {1} commits",
 
397
                                 number).format(fingerprint, number) )
 
398
        return result
 
399
 
 
400
    def valid_commits_message(self, count):
 
401
        """returns message for number of commits"""
 
402
        return i18n.gettext(u"{0} commits with valid signatures").format(
 
403
                                        count[SIGNATURE_VALID])
 
404
 
 
405
    def unknown_key_message(self, count):
 
406
        """returns message for number of commits"""
 
407
        return i18n.ngettext(u"{0} commit with unknown key",
 
408
                             u"{0} commits with unknown keys",
 
409
                             count[SIGNATURE_KEY_MISSING]).format(
 
410
                                        count[SIGNATURE_KEY_MISSING])
 
411
 
 
412
    def commit_not_valid_message(self, count):
 
413
        """returns message for number of commits"""
 
414
        return i18n.ngettext(u"{0} commit not valid",
 
415
                             u"{0} commits not valid",
 
416
                             count[SIGNATURE_NOT_VALID]).format(
 
417
                                            count[SIGNATURE_NOT_VALID])
 
418
 
 
419
    def commit_not_signed_message(self, count):
 
420
        """returns message for number of commits"""
 
421
        return i18n.ngettext(u"{0} commit not signed",
 
422
                             u"{0} commits not signed",
 
423
                             count[SIGNATURE_NOT_SIGNED]).format(
 
424
                                        count[SIGNATURE_NOT_SIGNED])