~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/gpg.py

  • Committer: John Arbash Meinel
  • Date: 2008-08-28 20:13:31 UTC
  • mfrom: (3658 +trunk)
  • mto: This revision was merged to the branch mainline in revision 3688.
  • Revision ID: john@arbash-meinel.com-20080828201331-dqffxf54l2heokll
Merge bzr.dev 3658

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2011 Canonical Ltd
 
1
# Copyright (C) 2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  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
23
22
 
24
23
from bzrlib.lazy_import import lazy_import
25
24
lazy_import(globals(), """
33
32
    )
34
33
""")
35
34
 
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
 
 
56
35
 
57
36
class DisabledGPGStrategy(object):
58
37
    """A GPG Strategy that makes everything fail."""
59
38
 
60
 
    @staticmethod
61
 
    def verify_signatures_available():
62
 
        return True
63
 
 
64
39
    def __init__(self, ignored):
65
40
        """Real strategies take a configuration."""
66
41
 
67
42
    def sign(self, content):
68
43
        raise errors.SigningFailed('Signing is disabled.')
69
44
 
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
 
 
77
45
 
78
46
class LoopbackGPGStrategy(object):
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
 
47
    """A GPG Strategy that acts like 'cat' - data is just passed through."""
86
48
 
87
49
    def __init__(self, ignored):
88
50
        """Real strategies take a configuration."""
91
53
        return ("-----BEGIN PSEUDO-SIGNED CONTENT-----\n" + content +
92
54
                "-----END PSEUDO-SIGNED CONTENT-----\n")
93
55
 
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])
144
 
 
145
56
 
146
57
def _set_gpg_tty():
147
58
    tty = os.environ.get('TTY')
158
69
 
159
70
class GPGStrategy(object):
160
71
    """GPG Signing and checking facilities."""
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
73
    def _command_line(self):
178
 
        
179
 
        return [self._config.gpg_signing_command(), '--clearsign', '-u',
180
 
                                                self._config.gpg_signing_key()]
 
74
        return [self._config.gpg_signing_command(), '--clearsign']
181
75
 
182
76
    def __init__(self, config):
183
77
        self._config = config
184
 
        try:
185
 
            import gpgme
186
 
            self.context = gpgme.Context()
187
 
        except ImportError, error:
188
 
            pass # can't use verify()
189
78
 
190
79
    def sign(self, content):
191
80
        if isinstance(content, unicode):
222
111
                raise errors.SigningFailed(self._command_line())
223
112
            else:
224
113
                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])