~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/gpg.py

  • Committer: John Arbash Meinel
  • Date: 2008-07-09 21:42:24 UTC
  • mto: This revision was merged to the branch mainline in revision 3543.
  • Revision ID: john@arbash-meinel.com-20080709214224-r75k87r6a01pfc3h
Restore a real weave merge to 'bzr merge --weave'.

To do so efficiently, we only add the simple LCAs to the final weave
object, unless we run into complexities with the merge graph.
This gives the same effective result as adding all the texts,
with the advantage of not having to extract all of them.

Show diffs side-by-side

added added

removed removed

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