18
18
"""GPG signing and checking logic."""
20
from __future__ import absolute_import
22
25
from breezy.lazy_import import lazy_import
23
26
lazy_import(globals(), """
24
30
from breezy import (
51
60
class GpgNotInstalled(errors.DependencyNotPresent):
53
_fmt = ('python-gpg is not installed, it is needed to create or '
54
'verify signatures. %(error)s')
62
_fmt = 'python-gpg is not installed, it is needed to verify signatures'
56
64
def __init__(self, error):
57
65
errors.DependencyNotPresent.__init__(self, 'gpg', error)
76
84
def bulk_verify_signatures(repository, revids, strategy,
77
process_events_callback=None):
85
process_events_callback=None):
78
86
"""Do verifications on a set of revisions
80
88
:param repository: repository object
143
151
"""Real strategies take a configuration."""
145
153
def sign(self, content, mode):
146
return (b"-----BEGIN PSEUDO-SIGNED CONTENT-----\n" + content
147
+ b"-----END PSEUDO-SIGNED CONTENT-----\n")
154
return ("-----BEGIN PSEUDO-SIGNED CONTENT-----\n" + content +
155
"-----END PSEUDO-SIGNED CONTENT-----\n")
149
157
def verify(self, signed_data, signature=None):
150
plain_text = signed_data.replace(
151
b"-----BEGIN PSEUDO-SIGNED CONTENT-----\n", b"")
152
plain_text = plain_text.replace(
153
b"-----END PSEUDO-SIGNED CONTENT-----\n", b"")
158
plain_text = signed_data.replace("-----BEGIN PSEUDO-SIGNED CONTENT-----\n", "")
159
plain_text = plain_text.replace("-----END PSEUDO-SIGNED CONTENT-----\n", "")
154
160
return SIGNATURE_VALID, None, plain_text
156
162
def set_acceptable_keys(self, command_line_input):
173
179
# This is not quite worthy of a warning, because some people
174
180
# don't need GPG_TTY to be set. But it is worthy of a big mark
175
# in brz.log, so that people can debug it if it happens to them
181
# in ~/.brz.log, so that people can debug it if it happens to them
176
182
trace.mutter('** Env var TTY empty, cannot set GPG_TTY.'
177
183
' Is TTY exported?')
189
195
self.context = gpg.Context()
190
196
self.context.armor = True
191
197
self.context.signers = self._get_signing_keys()
193
pass # can't use verify()
198
except ImportError as error:
199
pass # can't use verify()
195
201
def _get_signing_keys(self):
197
203
keyname = self._config_stack.get('gpg_signing_key')
198
if keyname == 'default':
199
# Leave things to gpg
204
206
return [self.context.get_key(keyname)]
205
207
except gpg.errors.KeyNotFound:
209
# not setting gpg_signing_key at all means we should
210
if keyname is None or keyname == 'default':
211
# 'default' or not setting gpg_signing_key at all means we should
210
212
# use the user email address
211
keyname = config.extract_email_address(
212
self._config_stack.get('email'))
213
if keyname == 'default':
213
keyname = config.extract_email_address(self._config_stack.get('email'))
215
214
possible_keys = self.context.keylist(keyname, secret=True)
217
return [next(possible_keys)]
216
return [possible_keys.next()]
218
217
except StopIteration:
226
225
:return: boolean if this strategy can verify signatures
229
import gpg # noqa: F401
230
except ImportError as error:
234
233
def sign(self, content, mode):
237
except ImportError as error:
238
raise GpgNotInstalled(
239
'Set create_signatures=no to disable creating signatures.')
241
if isinstance(content, str):
235
if isinstance(content, unicode):
242
236
raise errors.BzrBadParameterUnicode('content')
244
238
plain_text = gpg.Data(content)
276
269
except gpg.errors.BadSignatures as error:
277
270
fingerprint = error.result.signatures[0].fpr
278
271
if error.result.signatures[0].summary & gpg.constants.SIGSUM_KEY_EXPIRED:
279
expires = self.context.get_key(
280
error.result.signatures[0].fpr).subkeys[0].expires
272
expires = self.context.get_key(error.result.signatures[0].fpr).subkeys[0].expires
281
273
if expires > error.result.signatures[0].timestamp:
282
274
# The expired key was not expired at time of signing.
283
275
# test_verify_expired_but_valid()
290
282
# GPG does not know this key.
291
283
# test_verify_unknown_key()
292
if (error.result.signatures[0].summary &
293
gpg.constants.SIGSUM_KEY_MISSING):
284
if error.result.signatures[0].summary & gpg.constants.SIGSUM_KEY_MISSING:
294
285
return SIGNATURE_KEY_MISSING, fingerprint[-8:], None
296
287
return SIGNATURE_NOT_VALID, None, None
306
297
# it. test_verify_unacceptable_key()
307
298
fingerprint = result.signatures[0].fpr
308
299
if self.acceptable_keys is not None:
309
if fingerprint not in self.acceptable_keys:
300
if not fingerprint in self.acceptable_keys:
310
301
return SIGNATURE_KEY_MISSING, fingerprint[-8:], plain_output
311
302
# Yay gpg set the valid bit.
312
303
# Can't write a test for this one as you can't set a key to be
314
305
if result.signatures[0].summary & gpg.constants.SIGSUM_VALID:
315
306
key = self.context.get_key(fingerprint)
316
307
name = key.uids[0].name
317
if isinstance(name, bytes):
318
name = name.decode('utf-8')
319
308
email = key.uids[0].email
320
if isinstance(email, bytes):
321
email = email.decode('utf-8')
322
return (SIGNATURE_VALID, name + u" <" + email + u">", plain_output)
309
return SIGNATURE_VALID, name.decode('utf-8') + u" <" + email.decode('utf-8') + u">", plain_output
323
310
# Sigsum_red indicates a problem, unfortunatly I have not been able
324
311
# to write any tests which actually set this.
325
312
if result.signatures[0].summary & gpg.constants.SIGSUM_RED:
326
313
return SIGNATURE_NOT_VALID, None, plain_output
327
314
# Summary isn't set if sig is valid but key is untrusted but if user
328
315
# has explicity set the key as acceptable we can validate it.
329
if (result.signatures[0].summary == 0 and
330
self.acceptable_keys is not None):
316
if result.signatures[0].summary == 0 and self.acceptable_keys is not None:
331
317
if fingerprint in self.acceptable_keys:
332
318
# test_verify_untrusted_but_accepted()
333
319
return SIGNATURE_VALID, None, plain_output
350
336
acceptable_keys_config = self._config_stack.get('acceptable_keys')
351
337
if acceptable_keys_config is not None:
352
338
patterns = acceptable_keys_config
353
if command_line_input is not None: # command line overrides config
339
if command_line_input is not None: # command line overrides config
354
340
patterns = command_line_input.split(',')
364
350
trace.mutter("Added acceptable key: " + key.subkeys[0].fpr)
365
351
if not found_key:
366
352
trace.note(gettext(
367
"No GnuPG key results for pattern: {0}"
353
"No GnuPG key results for pattern: {0}"
371
357
def valid_commits_message(count):
372
358
"""returns message for number of commits"""
373
359
return gettext(u"{0} commits with valid signatures").format(
374
count[SIGNATURE_VALID])
360
count[SIGNATURE_VALID])
377
363
def unknown_key_message(count):
379
365
return ngettext(u"{0} commit with unknown key",
380
366
u"{0} commits with unknown keys",
381
367
count[SIGNATURE_KEY_MISSING]).format(
382
count[SIGNATURE_KEY_MISSING])
368
count[SIGNATURE_KEY_MISSING])
385
371
def commit_not_valid_message(count):
387
373
return ngettext(u"{0} commit not valid",
388
374
u"{0} commits not valid",
389
375
count[SIGNATURE_NOT_VALID]).format(
390
count[SIGNATURE_NOT_VALID])
376
count[SIGNATURE_NOT_VALID])
393
379
def commit_not_signed_message(count):
395
381
return ngettext(u"{0} commit not signed",
396
382
u"{0} commits not signed",
397
383
count[SIGNATURE_NOT_SIGNED]).format(
398
count[SIGNATURE_NOT_SIGNED])
384
count[SIGNATURE_NOT_SIGNED])
401
387
def expired_commit_message(count):
403
389
return ngettext(u"{0} commit with key now expired",
404
390
u"{0} commits with key now expired",
405
391
count[SIGNATURE_EXPIRED]).format(
406
count[SIGNATURE_EXPIRED])
392
count[SIGNATURE_EXPIRED])
409
395
def verbose_expired_key_message(result, repo):
436
422
signers[uid] += 1
438
424
for uid, number in signers.items():
439
result.append(ngettext(u"{0} signed {1} commit",
440
u"{0} signed {1} commits",
441
number).format(uid, number))
425
result.append(ngettext(u"{0} signed {1} commit",
426
u"{0} signed {1} commits",
427
number).format(uid, number))