~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/gpg.py

  • Committer: Vincent Ladeuil
  • Date: 2012-01-18 14:09:19 UTC
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120118140919-rlvdrhpc0nq1lbwi
Change set/remove to require a lock for the branch config files.

This means that tests (or any plugin for that matter) do not requires an
explicit lock on the branch anymore to change a single option. This also
means the optimisation becomes "opt-in" and as such won't be as
spectacular as it may be and/or harder to get right (nothing fails
anymore).

This reduces the diff by ~300 lines.

Code/tests that were updating more than one config option is still taking
a lock to at least avoid some IOs and demonstrate the benefits through
the decreased number of hpss calls.

The duplication between BranchStack and BranchOnlyStack will be removed
once the same sharing is in place for local config files, at which point
the Stack class itself may be able to host the changes.

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
from __future__ import absolute_import
 
21
 
20
22
import os
21
23
import sys
 
24
from StringIO import StringIO
22
25
 
23
26
from bzrlib.lazy_import import lazy_import
24
27
lazy_import(globals(), """
26
29
import subprocess
27
30
 
28
31
from bzrlib import (
 
32
    config,
29
33
    errors,
30
34
    trace,
31
35
    ui,
32
36
    )
 
37
from bzrlib.i18n import (
 
38
    gettext, 
 
39
    ngettext,
 
40
    )
33
41
""")
34
42
 
 
43
#verification results
 
44
SIGNATURE_VALID = 0
 
45
SIGNATURE_KEY_MISSING = 1
 
46
SIGNATURE_NOT_VALID = 2
 
47
SIGNATURE_NOT_SIGNED = 3
 
48
SIGNATURE_EXPIRED = 4
 
49
 
35
50
 
36
51
class DisabledGPGStrategy(object):
37
52
    """A GPG Strategy that makes everything fail."""
38
53
 
 
54
    @staticmethod
 
55
    def verify_signatures_available():
 
56
        return True
 
57
 
39
58
    def __init__(self, ignored):
40
59
        """Real strategies take a configuration."""
41
60
 
42
61
    def sign(self, content):
43
62
        raise errors.SigningFailed('Signing is disabled.')
44
63
 
 
64
    def verify(self, content, testament):
 
65
        raise errors.SignatureVerificationFailed('Signature verification is \
 
66
disabled.')
 
67
 
 
68
    def set_acceptable_keys(self, command_line_input):
 
69
        pass
 
70
 
45
71
 
46
72
class LoopbackGPGStrategy(object):
47
 
    """A GPG Strategy that acts like 'cat' - data is just passed through."""
 
73
    """A GPG Strategy that acts like 'cat' - data is just passed through.
 
74
    Used in tests.
 
75
    """
 
76
 
 
77
    @staticmethod
 
78
    def verify_signatures_available():
 
79
        return True
48
80
 
49
81
    def __init__(self, ignored):
50
82
        """Real strategies take a configuration."""
53
85
        return ("-----BEGIN PSEUDO-SIGNED CONTENT-----\n" + content +
54
86
                "-----END PSEUDO-SIGNED CONTENT-----\n")
55
87
 
 
88
    def verify(self, content, testament):
 
89
        return SIGNATURE_VALID, None
 
90
 
 
91
    def set_acceptable_keys(self, command_line_input):
 
92
        if command_line_input is not None:
 
93
            patterns = command_line_input.split(",")
 
94
            self.acceptable_keys = []
 
95
            for pattern in patterns:
 
96
                if pattern == "unknown":
 
97
                    pass
 
98
                else:
 
99
                    self.acceptable_keys.append(pattern)
 
100
 
 
101
    def do_verifications(self, revisions, repository):
 
102
        count = {SIGNATURE_VALID: 0,
 
103
                 SIGNATURE_KEY_MISSING: 0,
 
104
                 SIGNATURE_NOT_VALID: 0,
 
105
                 SIGNATURE_NOT_SIGNED: 0,
 
106
                 SIGNATURE_EXPIRED: 0}
 
107
        result = []
 
108
        all_verifiable = True
 
109
        for rev_id in revisions:
 
110
            verification_result, uid =\
 
111
                repository.verify_revision_signature(rev_id,self)
 
112
            result.append([rev_id, verification_result, uid])
 
113
            count[verification_result] += 1
 
114
            if verification_result != SIGNATURE_VALID:
 
115
                all_verifiable = False
 
116
        return (count, result, all_verifiable)
 
117
 
 
118
    def valid_commits_message(self, count):
 
119
        return gettext(u"{0} commits with valid signatures").format(
 
120
                                        count[SIGNATURE_VALID])
 
121
 
 
122
    def unknown_key_message(self, count):
 
123
        return ngettext(u"{0} commit with unknown key",
 
124
                             u"{0} commits with unknown keys",
 
125
                             count[SIGNATURE_KEY_MISSING]).format(
 
126
                                        count[SIGNATURE_KEY_MISSING])
 
127
 
 
128
    def commit_not_valid_message(self, count):
 
129
        return ngettext(u"{0} commit not valid",
 
130
                             u"{0} commits not valid",
 
131
                             count[SIGNATURE_NOT_VALID]).format(
 
132
                                            count[SIGNATURE_NOT_VALID])
 
133
 
 
134
    def commit_not_signed_message(self, count):
 
135
        return ngettext(u"{0} commit not signed",
 
136
                             u"{0} commits not signed",
 
137
                             count[SIGNATURE_NOT_SIGNED]).format(
 
138
                                        count[SIGNATURE_NOT_SIGNED])
 
139
 
 
140
    def expired_commit_message(self, count):
 
141
        return ngettext(u"{0} commit with key now expired",
 
142
                        u"{0} commits with key now expired",
 
143
                        count[SIGNATURE_EXPIRED]).format(
 
144
                                        count[SIGNATURE_EXPIRED])
 
145
 
56
146
 
57
147
def _set_gpg_tty():
58
148
    tty = os.environ.get('TTY')
69
159
 
70
160
class GPGStrategy(object):
71
161
    """GPG Signing and checking facilities."""
72
 
        
 
162
 
 
163
    acceptable_keys = None
 
164
 
 
165
    def __init__(self, config_stack):
 
166
        self._config_stack = config_stack
 
167
        try:
 
168
            import gpgme
 
169
            self.context = gpgme.Context()
 
170
        except ImportError, error:
 
171
            pass # can't use verify()
 
172
 
 
173
    @staticmethod
 
174
    def verify_signatures_available():
 
175
        """
 
176
        check if this strategy can verify signatures
 
177
 
 
178
        :return: boolean if this strategy can verify signatures
 
179
        """
 
180
        try:
 
181
            import gpgme
 
182
            return True
 
183
        except ImportError, error:
 
184
            return False
 
185
 
73
186
    def _command_line(self):
74
 
        return [self._config.gpg_signing_command(), '--clearsign']
75
 
 
76
 
    def __init__(self, config):
77
 
        self._config = config
 
187
        key = self._config_stack.get('gpg_signing_key')
 
188
        if key is None or key == 'default':
 
189
            # 'default' or not setting gpg_signing_key at all means we should
 
190
            # use the user email address
 
191
            key = config.extract_email_address(self._config_stack.get('email'))
 
192
        return [self._config_stack.get('gpg_signing_command'), '--clearsign',
 
193
                '-u', key]
78
194
 
79
195
    def sign(self, content):
80
196
        if isinstance(content, unicode):
111
227
                raise errors.SigningFailed(self._command_line())
112
228
            else:
113
229
                raise
 
230
 
 
231
    def verify(self, content, testament):
 
232
        """Check content has a valid signature.
 
233
        
 
234
        :param content: the commit signature
 
235
        :param testament: the valid testament string for the commit
 
236
        
 
237
        :return: SIGNATURE_VALID or a failed SIGNATURE_ value, key uid if valid
 
238
        """
 
239
        try:
 
240
            import gpgme
 
241
        except ImportError, error:
 
242
            raise errors.GpgmeNotInstalled(error)
 
243
 
 
244
        signature = StringIO(content)
 
245
        plain_output = StringIO()
 
246
        try:
 
247
            result = self.context.verify(signature, None, plain_output)
 
248
        except gpgme.GpgmeError,error:
 
249
            raise errors.SignatureVerificationFailed(error[2])
 
250
 
 
251
        # No result if input is invalid.
 
252
        # test_verify_invalid()
 
253
        if len(result) == 0:
 
254
            return SIGNATURE_NOT_VALID, None
 
255
        # User has specified a list of acceptable keys, check our result is in
 
256
        # it.  test_verify_unacceptable_key()
 
257
        fingerprint = result[0].fpr
 
258
        if self.acceptable_keys is not None:
 
259
            if not fingerprint in self.acceptable_keys:
 
260
                return SIGNATURE_KEY_MISSING, fingerprint[-8:]
 
261
        # Check the signature actually matches the testament.
 
262
        # test_verify_bad_testament()
 
263
        if testament != plain_output.getvalue():
 
264
            return SIGNATURE_NOT_VALID, None
 
265
        # Yay gpgme set the valid bit.
 
266
        # Can't write a test for this one as you can't set a key to be
 
267
        # trusted using gpgme.
 
268
        if result[0].summary & gpgme.SIGSUM_VALID:
 
269
            key = self.context.get_key(fingerprint)
 
270
            name = key.uids[0].name
 
271
            email = key.uids[0].email
 
272
            return SIGNATURE_VALID, name + " <" + email + ">"
 
273
        # Sigsum_red indicates a problem, unfortunatly I have not been able
 
274
        # to write any tests which actually set this.
 
275
        if result[0].summary & gpgme.SIGSUM_RED:
 
276
            return SIGNATURE_NOT_VALID, None
 
277
        # GPG does not know this key.
 
278
        # test_verify_unknown_key()
 
279
        if result[0].summary & gpgme.SIGSUM_KEY_MISSING:
 
280
            return SIGNATURE_KEY_MISSING, fingerprint[-8:]
 
281
        # Summary isn't set if sig is valid but key is untrusted but if user
 
282
        # has explicity set the key as acceptable we can validate it.
 
283
        if result[0].summary == 0 and self.acceptable_keys is not None:
 
284
            if fingerprint in self.acceptable_keys:
 
285
                # test_verify_untrusted_but_accepted()
 
286
                return SIGNATURE_VALID, None
 
287
        # test_verify_valid_but_untrusted()
 
288
        if result[0].summary == 0 and self.acceptable_keys is None:
 
289
            return SIGNATURE_NOT_VALID, None
 
290
        if result[0].summary & gpgme.SIGSUM_KEY_EXPIRED:
 
291
            expires = self.context.get_key(result[0].fpr).subkeys[0].expires
 
292
            if expires > result[0].timestamp:
 
293
                # The expired key was not expired at time of signing.
 
294
                # test_verify_expired_but_valid()
 
295
                return SIGNATURE_EXPIRED, fingerprint[-8:]
 
296
            else:
 
297
                # I can't work out how to create a test where the signature
 
298
                # was expired at the time of signing.
 
299
                return SIGNATURE_NOT_VALID, None
 
300
        # A signature from a revoked key gets this.
 
301
        # test_verify_revoked_signature()
 
302
        if result[0].summary & gpgme.SIGSUM_SYS_ERROR:
 
303
            return SIGNATURE_NOT_VALID, None
 
304
        # Other error types such as revoked keys should (I think) be caught by
 
305
        # SIGSUM_RED so anything else means something is buggy.
 
306
        raise errors.SignatureVerificationFailed("Unknown GnuPG key "\
 
307
                                                 "verification result")
 
308
 
 
309
    def set_acceptable_keys(self, command_line_input):
 
310
        """Set the acceptable keys for verifying with this GPGStrategy.
 
311
        
 
312
        :param command_line_input: comma separated list of patterns from
 
313
                                command line
 
314
        :return: nothing
 
315
        """
 
316
        key_patterns = None
 
317
        acceptable_keys_config = self._config_stack.get('acceptable_keys')
 
318
        try:
 
319
            if isinstance(acceptable_keys_config, unicode):
 
320
                acceptable_keys_config = str(acceptable_keys_config)
 
321
        except UnicodeEncodeError:
 
322
            # gpg Context.keylist(pattern) does not like unicode
 
323
            raise errors.BzrCommandError(
 
324
                gettext('Only ASCII permitted in option names'))
 
325
 
 
326
        if acceptable_keys_config is not None:
 
327
            key_patterns = acceptable_keys_config
 
328
        if command_line_input is not None: # command line overrides config
 
329
            key_patterns = command_line_input
 
330
        if key_patterns is not None:
 
331
            patterns = key_patterns.split(",")
 
332
 
 
333
            self.acceptable_keys = []
 
334
            for pattern in patterns:
 
335
                result = self.context.keylist(pattern)
 
336
                found_key = False
 
337
                for key in result:
 
338
                    found_key = True
 
339
                    self.acceptable_keys.append(key.subkeys[0].fpr)
 
340
                    trace.mutter("Added acceptable key: " + key.subkeys[0].fpr)
 
341
                if not found_key:
 
342
                    trace.note(gettext(
 
343
                            "No GnuPG key results for pattern: {0}"
 
344
                                ).format(pattern))
 
345
 
 
346
    def do_verifications(self, revisions, repository,
 
347
                            process_events_callback=None):
 
348
        """do verifications on a set of revisions
 
349
        
 
350
        :param revisions: list of revision ids to verify
 
351
        :param repository: repository object
 
352
        :param process_events_callback: method to call for GUI frontends that
 
353
                                                want to keep their UI refreshed
 
354
        
 
355
        :return: count dictionary of results of each type,
 
356
                 result list for each revision,
 
357
                 boolean True if all results are verified successfully
 
358
        """
 
359
        count = {SIGNATURE_VALID: 0,
 
360
                 SIGNATURE_KEY_MISSING: 0,
 
361
                 SIGNATURE_NOT_VALID: 0,
 
362
                 SIGNATURE_NOT_SIGNED: 0,
 
363
                 SIGNATURE_EXPIRED: 0}
 
364
        result = []
 
365
        all_verifiable = True
 
366
        for rev_id in revisions:
 
367
            verification_result, uid =\
 
368
                repository.verify_revision_signature(rev_id, self)
 
369
            result.append([rev_id, verification_result, uid])
 
370
            count[verification_result] += 1
 
371
            if verification_result != SIGNATURE_VALID:
 
372
                all_verifiable = False
 
373
            if process_events_callback is not None:
 
374
                process_events_callback()
 
375
        return (count, result, all_verifiable)
 
376
 
 
377
    def verbose_valid_message(self, result):
 
378
        """takes a verify result and returns list of signed commits strings"""
 
379
        signers = {}
 
380
        for rev_id, validity, uid in result:
 
381
            if validity == SIGNATURE_VALID:
 
382
                signers.setdefault(uid, 0)
 
383
                signers[uid] += 1
 
384
        result = []
 
385
        for uid, number in signers.items():
 
386
             result.append( ngettext(u"{0} signed {1} commit",
 
387
                             u"{0} signed {1} commits",
 
388
                             number).format(uid, number) )
 
389
        return result
 
390
 
 
391
 
 
392
    def verbose_not_valid_message(self, result, repo):
 
393
        """takes a verify result and returns list of not valid commit info"""
 
394
        signers = {}
 
395
        for rev_id, validity, empty in result:
 
396
            if validity == SIGNATURE_NOT_VALID:
 
397
                revision = repo.get_revision(rev_id)
 
398
                authors = ', '.join(revision.get_apparent_authors())
 
399
                signers.setdefault(authors, 0)
 
400
                signers[authors] += 1
 
401
        result = []
 
402
        for authors, number in signers.items():
 
403
            result.append( ngettext(u"{0} commit by author {1}",
 
404
                                 u"{0} commits by author {1}",
 
405
                                 number).format(number, authors) )
 
406
        return result
 
407
 
 
408
    def verbose_not_signed_message(self, result, repo):
 
409
        """takes a verify result and returns list of not signed commit info"""
 
410
        signers = {}
 
411
        for rev_id, validity, empty in result:
 
412
            if validity == SIGNATURE_NOT_SIGNED:
 
413
                revision = repo.get_revision(rev_id)
 
414
                authors = ', '.join(revision.get_apparent_authors())
 
415
                signers.setdefault(authors, 0)
 
416
                signers[authors] += 1
 
417
        result = []
 
418
        for authors, number in signers.items():
 
419
            result.append( ngettext(u"{0} commit by author {1}",
 
420
                                 u"{0} commits by author {1}",
 
421
                                 number).format(number, authors) )
 
422
        return result
 
423
 
 
424
    def verbose_missing_key_message(self, result):
 
425
        """takes a verify result and returns list of missing key info"""
 
426
        signers = {}
 
427
        for rev_id, validity, fingerprint in result:
 
428
            if validity == SIGNATURE_KEY_MISSING:
 
429
                signers.setdefault(fingerprint, 0)
 
430
                signers[fingerprint] += 1
 
431
        result = []
 
432
        for fingerprint, number in signers.items():
 
433
            result.append( ngettext(u"Unknown key {0} signed {1} commit",
 
434
                                 u"Unknown key {0} signed {1} commits",
 
435
                                 number).format(fingerprint, number) )
 
436
        return result
 
437
 
 
438
    def verbose_expired_key_message(self, result, repo):
 
439
        """takes a verify result and returns list of expired key info"""
 
440
        signers = {}
 
441
        fingerprint_to_authors = {}
 
442
        for rev_id, validity, fingerprint in result:
 
443
            if validity == SIGNATURE_EXPIRED:
 
444
                revision = repo.get_revision(rev_id)
 
445
                authors = ', '.join(revision.get_apparent_authors())
 
446
                signers.setdefault(fingerprint, 0)
 
447
                signers[fingerprint] += 1
 
448
                fingerprint_to_authors[fingerprint] = authors
 
449
        result = []
 
450
        for fingerprint, number in signers.items():
 
451
            result.append(
 
452
                ngettext(u"{0} commit by author {1} with key {2} now expired",
 
453
                         u"{0} commits by author {1} with key {2} now expired",
 
454
                         number).format(
 
455
                    number, fingerprint_to_authors[fingerprint], fingerprint) )
 
456
        return result
 
457
 
 
458
    def valid_commits_message(self, count):
 
459
        """returns message for number of commits"""
 
460
        return gettext(u"{0} commits with valid signatures").format(
 
461
                                        count[SIGNATURE_VALID])
 
462
 
 
463
    def unknown_key_message(self, count):
 
464
        """returns message for number of commits"""
 
465
        return ngettext(u"{0} commit with unknown key",
 
466
                        u"{0} commits with unknown keys",
 
467
                        count[SIGNATURE_KEY_MISSING]).format(
 
468
                                        count[SIGNATURE_KEY_MISSING])
 
469
 
 
470
    def commit_not_valid_message(self, count):
 
471
        """returns message for number of commits"""
 
472
        return ngettext(u"{0} commit not valid",
 
473
                        u"{0} commits not valid",
 
474
                        count[SIGNATURE_NOT_VALID]).format(
 
475
                                            count[SIGNATURE_NOT_VALID])
 
476
 
 
477
    def commit_not_signed_message(self, count):
 
478
        """returns message for number of commits"""
 
479
        return ngettext(u"{0} commit not signed",
 
480
                        u"{0} commits not signed",
 
481
                        count[SIGNATURE_NOT_SIGNED]).format(
 
482
                                        count[SIGNATURE_NOT_SIGNED])
 
483
 
 
484
    def expired_commit_message(self, count):
 
485
        """returns message for number of commits"""
 
486
        return ngettext(u"{0} commit with key now expired",
 
487
                        u"{0} commits with key now expired",
 
488
                        count[SIGNATURE_EXPIRED]).format(
 
489
                                    count[SIGNATURE_EXPIRED])