~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/gpg.py

  • Committer: Patch Queue Manager
  • Date: 2015-12-17 18:39:00 UTC
  • mfrom: (6606.1.2 fix-float)
  • Revision ID: pqm@pqm.ubuntu.com-20151217183900-0719du2uv1kwu3lc
(vila) Inline testtools private method to fix an issue in xenial (the
 private implementation has changed in an backward incompatible way).
 (Jelmer Vernooij)

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
22
24
from StringIO import StringIO
27
29
import subprocess
28
30
 
29
31
from bzrlib import (
 
32
    config,
30
33
    errors,
31
34
    trace,
32
35
    ui,
37
40
    )
38
41
""")
39
42
 
 
43
from bzrlib.symbol_versioning import (
 
44
    deprecated_in,
 
45
    deprecated_method,
 
46
    )
 
47
 
40
48
#verification results
41
49
SIGNATURE_VALID = 0
42
50
SIGNATURE_KEY_MISSING = 1
45
53
SIGNATURE_EXPIRED = 4
46
54
 
47
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
 
48
95
class DisabledGPGStrategy(object):
49
96
    """A GPG Strategy that makes everything fail."""
50
97
 
95
142
                else:
96
143
                    self.acceptable_keys.append(pattern)
97
144
 
 
145
    @deprecated_method(deprecated_in((2, 6, 0)))
98
146
    def do_verifications(self, revisions, repository):
99
 
        count = {SIGNATURE_VALID: 0,
100
 
                 SIGNATURE_KEY_MISSING: 0,
101
 
                 SIGNATURE_NOT_VALID: 0,
102
 
                 SIGNATURE_NOT_SIGNED: 0,
103
 
                 SIGNATURE_EXPIRED: 0}
104
 
        result = []
105
 
        all_verifiable = True
106
 
        for rev_id in revisions:
107
 
            verification_result, uid =\
108
 
                                repository.verify_revision(rev_id,self)
109
 
            result.append([rev_id, verification_result, uid])
110
 
            count[verification_result] += 1
111
 
            if verification_result != SIGNATURE_VALID:
112
 
                all_verifiable = False
113
 
        return (count, result, all_verifiable)
 
147
        return bulk_verify_signatures(repository, revisions, self)
114
148
 
 
149
    @deprecated_method(deprecated_in((2, 6, 0)))
115
150
    def valid_commits_message(self, count):
116
 
        return gettext(u"{0} commits with valid signatures").format(
117
 
                                        count[SIGNATURE_VALID])            
 
151
        return valid_commits_message(count)
118
152
 
 
153
    @deprecated_method(deprecated_in((2, 6, 0)))
119
154
    def unknown_key_message(self, count):
120
 
        return ngettext(u"{0} commit with unknown key",
121
 
                             u"{0} commits with unknown keys",
122
 
                             count[SIGNATURE_KEY_MISSING]).format(
123
 
                                        count[SIGNATURE_KEY_MISSING])
 
155
        return unknown_key_message(count)
124
156
 
 
157
    @deprecated_method(deprecated_in((2, 6, 0)))
125
158
    def commit_not_valid_message(self, count):
126
 
        return ngettext(u"{0} commit not valid",
127
 
                             u"{0} commits not valid",
128
 
                             count[SIGNATURE_NOT_VALID]).format(
129
 
                                            count[SIGNATURE_NOT_VALID])
 
159
        return commit_not_valid_message(count)
130
160
 
 
161
    @deprecated_method(deprecated_in((2, 6, 0)))
131
162
    def commit_not_signed_message(self, count):
132
 
        return ngettext(u"{0} commit not signed",
133
 
                             u"{0} commits not signed",
134
 
                             count[SIGNATURE_NOT_SIGNED]).format(
135
 
                                        count[SIGNATURE_NOT_SIGNED])
 
163
        return commit_not_signed_message(count)
136
164
 
 
165
    @deprecated_method(deprecated_in((2, 6, 0)))
137
166
    def expired_commit_message(self, count):
138
 
        return ngettext(u"{0} commit with key now expired",
139
 
                        u"{0} commits with key now expired",
140
 
                        count[SIGNATURE_EXPIRED]).format(
141
 
                                        count[SIGNATURE_EXPIRED])
 
167
        return expired_commit_message(count)
142
168
 
143
169
 
144
170
def _set_gpg_tty():
159
185
 
160
186
    acceptable_keys = None
161
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
 
162
196
    @staticmethod
163
197
    def verify_signatures_available():
164
198
        """
173
207
            return False
174
208
 
175
209
    def _command_line(self):
176
 
        
177
 
        return [self._config.gpg_signing_command(), '--clearsign', '-u',
178
 
                                                self._config.gpg_signing_key()]
179
 
 
180
 
    def __init__(self, config):
181
 
        self._config = config
182
 
        try:
183
 
            import gpgme
184
 
            self.context = gpgme.Context()
185
 
        except ImportError, error:
186
 
            pass # can't use verify()
 
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]
187
217
 
188
218
    def sign(self, content):
189
219
        if isinstance(content, unicode):
223
253
 
224
254
    def verify(self, content, testament):
225
255
        """Check content has a valid signature.
226
 
        
 
256
 
227
257
        :param content: the commit signature
228
258
        :param testament: the valid testament string for the commit
229
 
        
 
259
 
230
260
        :return: SIGNATURE_VALID or a failed SIGNATURE_ value, key uid if valid
231
261
        """
232
262
        try:
236
266
 
237
267
        signature = StringIO(content)
238
268
        plain_output = StringIO()
239
 
        
240
269
        try:
241
270
            result = self.context.verify(signature, None, plain_output)
242
271
        except gpgme.GpgmeError,error:
246
275
        # test_verify_invalid()
247
276
        if len(result) == 0:
248
277
            return SIGNATURE_NOT_VALID, None
249
 
        # User has specified a list of acceptable keys, check our result is in it.
250
 
        # test_verify_unacceptable_key()
 
278
        # User has specified a list of acceptable keys, check our result is in
 
279
        # it.  test_verify_unacceptable_key()
251
280
        fingerprint = result[0].fpr
252
281
        if self.acceptable_keys is not None:
253
 
            if not fingerprint in self.acceptable_keys:                
 
282
            if not fingerprint in self.acceptable_keys:
254
283
                return SIGNATURE_KEY_MISSING, fingerprint[-8:]
255
284
        # Check the signature actually matches the testament.
256
285
        # test_verify_bad_testament()
257
286
        if testament != plain_output.getvalue():
258
 
            return SIGNATURE_NOT_VALID, None 
 
287
            return SIGNATURE_NOT_VALID, None
259
288
        # Yay gpgme set the valid bit.
260
289
        # Can't write a test for this one as you can't set a key to be
261
290
        # trusted using gpgme.
272
301
        # test_verify_unknown_key()
273
302
        if result[0].summary & gpgme.SIGSUM_KEY_MISSING:
274
303
            return SIGNATURE_KEY_MISSING, fingerprint[-8:]
275
 
        # Summary isn't set if sig is valid but key is untrusted
276
 
        # but if user has explicity set the key as acceptable we can validate it.
 
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.
277
306
        if result[0].summary == 0 and self.acceptable_keys is not None:
278
307
            if fingerprint in self.acceptable_keys:
279
308
                # test_verify_untrusted_but_accepted()
280
 
                return SIGNATURE_VALID, None 
 
309
                return SIGNATURE_VALID, None
281
310
        # test_verify_valid_but_untrusted()
282
311
        if result[0].summary == 0 and self.acceptable_keys is None:
283
312
            return SIGNATURE_NOT_VALID, None
301
330
                                                 "verification result")
302
331
 
303
332
    def set_acceptable_keys(self, command_line_input):
304
 
        """sets the acceptable keys for verifying with this GPGStrategy
305
 
        
 
333
        """Set the acceptable keys for verifying with this GPGStrategy.
 
334
 
306
335
        :param command_line_input: comma separated list of patterns from
307
336
                                command line
308
337
        :return: nothing
309
338
        """
310
 
        key_patterns = None
311
 
        acceptable_keys_config = self._config.acceptable_keys()
312
 
        try:
313
 
            if isinstance(acceptable_keys_config, unicode):
314
 
                acceptable_keys_config = str(acceptable_keys_config)
315
 
        except UnicodeEncodeError:
316
 
            #gpg Context.keylist(pattern) does not like unicode
317
 
            raise errors.BzrCommandError(gettext('Only ASCII permitted in option names'))
318
 
 
 
339
        patterns = None
 
340
        acceptable_keys_config = self._config_stack.get('acceptable_keys')
319
341
        if acceptable_keys_config is not None:
320
 
            key_patterns = acceptable_keys_config
321
 
        if command_line_input is not None: #command line overrides config
322
 
            key_patterns = command_line_input
323
 
        if key_patterns is not None:
324
 
            patterns = key_patterns.split(",")
 
342
            patterns = acceptable_keys_config
 
343
        if command_line_input is not None: # command line overrides config
 
344
            patterns = command_line_input.split(',')
325
345
 
 
346
        if patterns:
326
347
            self.acceptable_keys = []
327
348
            for pattern in patterns:
328
349
                result = self.context.keylist(pattern)
336
357
                            "No GnuPG key results for pattern: {0}"
337
358
                                ).format(pattern))
338
359
 
 
360
    @deprecated_method(deprecated_in((2, 6, 0)))
339
361
    def do_verifications(self, revisions, repository,
340
362
                            process_events_callback=None):
341
363
        """do verifications on a set of revisions
342
 
        
 
364
 
343
365
        :param revisions: list of revision ids to verify
344
366
        :param repository: repository object
345
367
        :param process_events_callback: method to call for GUI frontends that
346
 
                                                want to keep their UI refreshed
347
 
        
 
368
            want to keep their UI refreshed
 
369
 
348
370
        :return: count dictionary of results of each type,
349
371
                 result list for each revision,
350
372
                 boolean True if all results are verified successfully
351
373
        """
352
 
        count = {SIGNATURE_VALID: 0,
353
 
                 SIGNATURE_KEY_MISSING: 0,
354
 
                 SIGNATURE_NOT_VALID: 0,
355
 
                 SIGNATURE_NOT_SIGNED: 0,
356
 
                 SIGNATURE_EXPIRED: 0}
357
 
        result = []
358
 
        all_verifiable = True
359
 
        for rev_id in revisions:
360
 
            verification_result, uid =\
361
 
                                repository.verify_revision(rev_id,self)
362
 
            result.append([rev_id, verification_result, uid])
363
 
            count[verification_result] += 1
364
 
            if verification_result != SIGNATURE_VALID:
365
 
                all_verifiable = False
366
 
            if process_events_callback is not None:
367
 
                process_events_callback()
368
 
        return (count, result, all_verifiable)
 
374
        return bulk_verify_signatures(repository, revisions, self,
 
375
            process_events_callback)
369
376
 
 
377
    @deprecated_method(deprecated_in((2, 6, 0)))
370
378
    def verbose_valid_message(self, result):
371
379
        """takes a verify result and returns list of signed commits strings"""
372
 
        signers = {}
373
 
        for rev_id, validity, uid in result:
374
 
            if validity == SIGNATURE_VALID:
375
 
                signers.setdefault(uid, 0)
376
 
                signers[uid] += 1
377
 
        result = []
378
 
        for uid, number in signers.items():
379
 
             result.append( ngettext(u"{0} signed {1} commit", 
380
 
                             u"{0} signed {1} commits",
381
 
                             number).format(uid, number) )
382
 
        return result
383
 
 
384
 
 
 
380
        return verbose_valid_message(result)
 
381
 
 
382
    @deprecated_method(deprecated_in((2, 6, 0)))
385
383
    def verbose_not_valid_message(self, result, repo):
386
384
        """takes a verify result and returns list of not valid commit info"""
387
 
        signers = {}
388
 
        for rev_id, validity, empty in result:
389
 
            if validity == SIGNATURE_NOT_VALID:
390
 
                revision = repo.get_revision(rev_id)
391
 
                authors = ', '.join(revision.get_apparent_authors())
392
 
                signers.setdefault(authors, 0)
393
 
                signers[authors] += 1
394
 
        result = []
395
 
        for authors, number in signers.items():
396
 
            result.append( ngettext(u"{0} commit by author {1}", 
397
 
                                 u"{0} commits by author {1}",
398
 
                                 number).format(number, authors) )
399
 
        return result
 
385
        return verbose_not_valid_message(result, repo)
400
386
 
 
387
    @deprecated_method(deprecated_in((2, 6, 0)))
401
388
    def verbose_not_signed_message(self, result, repo):
402
389
        """takes a verify result and returns list of not signed commit info"""
403
 
        signers = {}
404
 
        for rev_id, validity, empty in result:
405
 
            if validity == SIGNATURE_NOT_SIGNED:
406
 
                revision = repo.get_revision(rev_id)
407
 
                authors = ', '.join(revision.get_apparent_authors())
408
 
                signers.setdefault(authors, 0)
409
 
                signers[authors] += 1
410
 
        result = []
411
 
        for authors, number in signers.items():
412
 
            result.append( ngettext(u"{0} commit by author {1}", 
413
 
                                 u"{0} commits by author {1}",
414
 
                                 number).format(number, authors) )
415
 
        return result
 
390
        return verbose_not_valid_message(result, repo)
416
391
 
 
392
    @deprecated_method(deprecated_in((2, 6, 0)))
417
393
    def verbose_missing_key_message(self, result):
418
394
        """takes a verify result and returns list of missing key info"""
419
 
        signers = {}
420
 
        for rev_id, validity, fingerprint in result:
421
 
            if validity == SIGNATURE_KEY_MISSING:
422
 
                signers.setdefault(fingerprint, 0)
423
 
                signers[fingerprint] += 1
424
 
        result = []
425
 
        for fingerprint, number in signers.items():
426
 
            result.append( ngettext(u"Unknown key {0} signed {1} commit", 
427
 
                                 u"Unknown key {0} signed {1} commits",
428
 
                                 number).format(fingerprint, number) )
429
 
        return result
 
395
        return verbose_missing_key_message(result)
430
396
 
 
397
    @deprecated_method(deprecated_in((2, 6, 0)))
431
398
    def verbose_expired_key_message(self, result, repo):
432
399
        """takes a verify result and returns list of expired key info"""
433
 
        signers = {}
434
 
        fingerprint_to_authors = {}
435
 
        for rev_id, validity, fingerprint in result:
436
 
            if validity == SIGNATURE_EXPIRED:
437
 
                revision = repo.get_revision(rev_id)
438
 
                authors = ', '.join(revision.get_apparent_authors())
439
 
                signers.setdefault(fingerprint, 0)
440
 
                signers[fingerprint] += 1
441
 
                fingerprint_to_authors[fingerprint] = authors
442
 
        result = []
443
 
        for fingerprint, number in signers.items():
444
 
            result.append(ngettext(u"{0} commit by author {1} with "\
445
 
                                    "key {2} now expired", 
446
 
                                   u"{0} commits by author {1} with key {2} now "\
447
 
                                    "expired",
448
 
                                    number).format(number,
449
 
                            fingerprint_to_authors[fingerprint], fingerprint) )
450
 
        return result
 
400
        return verbose_expired_key_message(result, repo)
451
401
 
 
402
    @deprecated_method(deprecated_in((2, 6, 0)))
452
403
    def valid_commits_message(self, count):
453
404
        """returns message for number of commits"""
454
 
        return gettext(u"{0} commits with valid signatures").format(
455
 
                                        count[SIGNATURE_VALID])
 
405
        return valid_commits_message(count)
456
406
 
 
407
    @deprecated_method(deprecated_in((2, 6, 0)))
457
408
    def unknown_key_message(self, count):
458
409
        """returns message for number of commits"""
459
 
        return ngettext(u"{0} commit with unknown key",
460
 
                        u"{0} commits with unknown keys",
461
 
                        count[SIGNATURE_KEY_MISSING]).format(
462
 
                                        count[SIGNATURE_KEY_MISSING])
 
410
        return unknown_key_message(count)
463
411
 
 
412
    @deprecated_method(deprecated_in((2, 6, 0)))
464
413
    def commit_not_valid_message(self, count):
465
414
        """returns message for number of commits"""
466
 
        return ngettext(u"{0} commit not valid",
467
 
                        u"{0} commits not valid",
468
 
                        count[SIGNATURE_NOT_VALID]).format(
469
 
                                            count[SIGNATURE_NOT_VALID])
 
415
        return commit_not_valid_message(count)
470
416
 
 
417
    @deprecated_method(deprecated_in((2, 6, 0)))
471
418
    def commit_not_signed_message(self, count):
472
419
        """returns message for number of commits"""
473
 
        return ngettext(u"{0} commit not signed",
474
 
                        u"{0} commits not signed",
475
 
                        count[SIGNATURE_NOT_SIGNED]).format(
476
 
                                        count[SIGNATURE_NOT_SIGNED])
 
420
        return commit_not_signed_message(count)
477
421
 
 
422
    @deprecated_method(deprecated_in((2, 6, 0)))
478
423
    def expired_commit_message(self, count):
479
424
        """returns message for number of commits"""
480
 
        return ngettext(u"{0} commit with key now expired",
481
 
                        u"{0} commits with key now expired",
482
 
                        count[SIGNATURE_EXPIRED]).format(
483
 
                                    count[SIGNATURE_EXPIRED])
 
425
        return expired_commit_message(count)
 
426
 
 
427
 
 
428
def valid_commits_message(count):
 
429
    """returns message for number of commits"""
 
430
    return gettext(u"{0} commits with valid signatures").format(
 
431
                                    count[SIGNATURE_VALID])
 
432
 
 
433
 
 
434
def unknown_key_message(count):
 
435
    """returns message for number of commits"""
 
436
    return ngettext(u"{0} commit with unknown key",
 
437
                    u"{0} commits with unknown keys",
 
438
                    count[SIGNATURE_KEY_MISSING]).format(
 
439
                                    count[SIGNATURE_KEY_MISSING])
 
440
 
 
441
 
 
442
def commit_not_valid_message(count):
 
443
    """returns message for number of commits"""
 
444
    return ngettext(u"{0} commit not valid",
 
445
                    u"{0} commits not valid",
 
446
                    count[SIGNATURE_NOT_VALID]).format(
 
447
                                        count[SIGNATURE_NOT_VALID])
 
448
 
 
449
 
 
450
def commit_not_signed_message(count):
 
451
    """returns message for number of commits"""
 
452
    return ngettext(u"{0} commit not signed",
 
453
                    u"{0} commits not signed",
 
454
                    count[SIGNATURE_NOT_SIGNED]).format(
 
455
                                    count[SIGNATURE_NOT_SIGNED])
 
456
 
 
457
 
 
458
def expired_commit_message(count):
 
459
    """returns message for number of commits"""
 
460
    return ngettext(u"{0} commit with key now expired",
 
461
                    u"{0} commits with key now expired",
 
462
                    count[SIGNATURE_EXPIRED]).format(
 
463
                                count[SIGNATURE_EXPIRED])
 
464
 
 
465
 
 
466
def verbose_expired_key_message(result, repo):
 
467
    """takes a verify result and returns list of expired key info"""
 
468
    signers = {}
 
469
    fingerprint_to_authors = {}
 
470
    for rev_id, validity, fingerprint in result:
 
471
        if validity == SIGNATURE_EXPIRED:
 
472
            revision = repo.get_revision(rev_id)
 
473
            authors = ', '.join(revision.get_apparent_authors())
 
474
            signers.setdefault(fingerprint, 0)
 
475
            signers[fingerprint] += 1
 
476
            fingerprint_to_authors[fingerprint] = authors
 
477
    result = []
 
478
    for fingerprint, number in signers.items():
 
479
        result.append(
 
480
            ngettext(u"{0} commit by author {1} with key {2} now expired",
 
481
                     u"{0} commits by author {1} with key {2} now expired",
 
482
                     number).format(
 
483
                number, fingerprint_to_authors[fingerprint], fingerprint))
 
484
    return result
 
485
 
 
486
 
 
487
def verbose_valid_message(result):
 
488
    """takes a verify result and returns list of signed commits strings"""
 
489
    signers = {}
 
490
    for rev_id, validity, uid in result:
 
491
        if validity == SIGNATURE_VALID:
 
492
            signers.setdefault(uid, 0)
 
493
            signers[uid] += 1
 
494
    result = []
 
495
    for uid, number in signers.items():
 
496
         result.append(ngettext(u"{0} signed {1} commit",
 
497
                                u"{0} signed {1} commits",
 
498
                                number).format(uid, number))
 
499
    return result
 
500
 
 
501
 
 
502
def verbose_not_valid_message(result, repo):
 
503
    """takes a verify result and returns list of not valid commit info"""
 
504
    signers = {}
 
505
    for rev_id, validity, empty in result:
 
506
        if validity == SIGNATURE_NOT_VALID:
 
507
            revision = repo.get_revision(rev_id)
 
508
            authors = ', '.join(revision.get_apparent_authors())
 
509
            signers.setdefault(authors, 0)
 
510
            signers[authors] += 1
 
511
    result = []
 
512
    for authors, number in signers.items():
 
513
        result.append(ngettext(u"{0} commit by author {1}",
 
514
                               u"{0} commits by author {1}",
 
515
                               number).format(number, authors))
 
516
    return result
 
517
 
 
518
 
 
519
def verbose_not_signed_message(result, repo):
 
520
    """takes a verify result and returns list of not signed commit info"""
 
521
    signers = {}
 
522
    for rev_id, validity, empty in result:
 
523
        if validity == SIGNATURE_NOT_SIGNED:
 
524
            revision = repo.get_revision(rev_id)
 
525
            authors = ', '.join(revision.get_apparent_authors())
 
526
            signers.setdefault(authors, 0)
 
527
            signers[authors] += 1
 
528
    result = []
 
529
    for authors, number in signers.items():
 
530
        result.append(ngettext(u"{0} commit by author {1}",
 
531
                               u"{0} commits by author {1}",
 
532
                               number).format(number, authors))
 
533
    return result
 
534
 
 
535
 
 
536
def verbose_missing_key_message(result):
 
537
    """takes a verify result and returns list of missing key info"""
 
538
    signers = {}
 
539
    for rev_id, validity, fingerprint in result:
 
540
        if validity == SIGNATURE_KEY_MISSING:
 
541
            signers.setdefault(fingerprint, 0)
 
542
            signers[fingerprint] += 1
 
543
    result = []
 
544
    for fingerprint, number in signers.items():
 
545
        result.append(ngettext(u"Unknown key {0} signed {1} commit",
 
546
                               u"Unknown key {0} signed {1} commits",
 
547
                               number).format(fingerprint, number))
 
548
    return result