Merge lp:~jelmer/brz/gpg-detached-sign into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/gpg-detached-sign
Merge into: lp:brz
Diff against target: 448 lines (+73/-111)
8 files modified
breezy/bzr/remote.py (+5/-3)
breezy/gpg.py (+39/-31)
breezy/merge_directive.py (+1/-1)
breezy/repository.py (+6/-5)
breezy/tests/per_repository/test_signatures.py (+1/-1)
breezy/tests/test_commit.py (+2/-1)
breezy/tests/test_gpg.py (+13/-54)
breezy/tests/test_log.py (+6/-15)
To merge this branch: bzr merge lp:~jelmer/brz/gpg-detached-sign
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+341287@code.launchpad.net

Commit message

Add support for passing mode argument to GPGSignature.sign.

Description of the change

Add support for passing mode argument to GPGSignature.sign.

This is necessary for brz-git, which needs to create detached signatures.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Thanks, so questions answered, constants are the same as the mapping, but the breezy interface doesn't depend on gpg module being installed. Also, argument not defaulted as bzr wants clear but git needs detached, so better to just pass.

review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
https://ci.breezy-vcs.org/job/brz-dev/41/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/bzr/remote.py'
2--- breezy/bzr/remote.py 2017-11-23 20:02:55 +0000
3+++ breezy/bzr/remote.py 2018-03-24 01:56:28 +0000
4@@ -2692,7 +2692,7 @@
5
6 def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
7 with self.lock_write():
8- signature = gpg_strategy.sign(plaintext)
9+ signature = gpg_strategy.sign(plaintext, gpg.MODE_CLEAR)
10 self.add_signature_text(revision_id, signature)
11
12 def add_signature_text(self, revision_id, signature):
13@@ -2737,9 +2737,11 @@
14 signature = self.get_signature_text(revision_id)
15
16 testament = _mod_testament.Testament.from_revision(self, revision_id)
17- plaintext = testament.as_short_text()
18
19- return gpg_strategy.verify(signature, plaintext)
20+ (status, key, signed_plaintext) = gpg_strategy.verify(signature)
21+ if testament.as_short_text() != signed_plaintext:
22+ return gpg.SIGNATURE_NOT_VALID, None
23+ return (status, key)
24
25 def item_keys_introduced_by(self, revision_ids, _files_pb=None):
26 self._ensure_real()
27
28=== modified file 'breezy/gpg.py'
29--- breezy/gpg.py 2018-02-24 15:50:23 +0000
30+++ breezy/gpg.py 2018-03-24 01:56:28 +0000
31@@ -52,6 +52,10 @@
32 SIGNATURE_NOT_SIGNED = 3
33 SIGNATURE_EXPIRED = 4
34
35+MODE_NORMAL = 0
36+MODE_DETACH = 1
37+MODE_CLEAR = 2
38+
39
40 class GpgNotInstalled(errors.DependencyNotPresent):
41
42@@ -123,10 +127,10 @@
43 def __init__(self, ignored):
44 """Real strategies take a configuration."""
45
46- def sign(self, content):
47+ def sign(self, content, mode):
48 raise SigningFailed('Signing is disabled.')
49
50- def verify(self, content, testament):
51+ def verify(self, signed_data, signature=None):
52 raise SignatureVerificationFailed('Signature verification is \
53 disabled.')
54
55@@ -146,12 +150,14 @@
56 def __init__(self, ignored):
57 """Real strategies take a configuration."""
58
59- def sign(self, content):
60+ def sign(self, content, mode):
61 return ("-----BEGIN PSEUDO-SIGNED CONTENT-----\n" + content +
62 "-----END PSEUDO-SIGNED CONTENT-----\n")
63
64- def verify(self, content, testament):
65- return SIGNATURE_VALID, None
66+ def verify(self, signed_data, signature=None):
67+ plain_text = signed_data.replace("-----BEGIN PSEUDO-SIGNED CONTENT-----\n", "")
68+ plain_text = plain_text.replace("-----END PSEUDO-SIGNED CONTENT-----\n", "")
69+ return SIGNATURE_VALID, None, plain_text
70
71 def set_acceptable_keys(self, command_line_input):
72 if command_line_input is not None:
73@@ -187,11 +193,11 @@
74 try:
75 import gpg
76 self.context = gpg.Context()
77+ self.context.armor = True
78+ self.context.signers = self._get_signing_keys()
79 except ImportError as error:
80 pass # can't use verify()
81
82- self.context.signers = self._get_signing_keys()
83-
84 def _get_signing_keys(self):
85 import gpg
86 keyname = self._config_stack.get('gpg_signing_key')
87@@ -224,7 +230,7 @@
88 except ImportError as error:
89 return False
90
91- def sign(self, content):
92+ def sign(self, content, mode):
93 import gpg
94 if isinstance(content, unicode):
95 raise errors.BzrBadParameterUnicode('content')
96@@ -232,29 +238,34 @@
97 plain_text = gpg.Data(content)
98 try:
99 output, result = self.context.sign(
100- plain_text, mode=gpg.constants.sig.mode.CLEAR)
101+ plain_text, mode={
102+ MODE_DETACH: gpg.constants.sig.mode.DETACH,
103+ MODE_CLEAR: gpg.constants.sig.mode.CLEAR,
104+ MODE_NORMAL: gpg.constants.sig.mode.NORMAL,
105+ }[mode])
106 except gpg.errors.GPGMEError as error:
107 raise SigningFailed(str(error))
108
109 return output
110
111- def verify(self, content, testament):
112+ def verify(self, signed_data, signature=None):
113 """Check content has a valid signature.
114
115- :param content: the commit signature
116- :param testament: the valid testament string for the commit
117+ :param signed_data; Signed data
118+ :param signature: optional signature (if detached)
119
120- :return: SIGNATURE_VALID or a failed SIGNATURE_ value, key uid if valid
121+ :return: SIGNATURE_VALID or a failed SIGNATURE_ value, key uid if valid, plain text
122 """
123 try:
124 import gpg
125 except ImportError as error:
126 raise errors.GpgNotInstalled(error)
127
128- signature = gpg.Data(content)
129- sink = gpg.Data()
130+ signed_data = gpg.Data(signed_data)
131+ if signature:
132+ signature = gpg.Data(signature)
133 try:
134- plain_output, result = self.context.verify(signature)
135+ plain_output, result = self.context.verify(signed_data, signature)
136 except gpg.errors.BadSignatures as error:
137 fingerprint = error.result.signatures[0].fpr
138 if error.result.signatures[0].summary & gpg.constants.SIGSUM_KEY_EXPIRED:
139@@ -262,35 +273,32 @@
140 if expires > error.result.signatures[0].timestamp:
141 # The expired key was not expired at time of signing.
142 # test_verify_expired_but_valid()
143- return SIGNATURE_EXPIRED, fingerprint[-8:]
144+ return SIGNATURE_EXPIRED, fingerprint[-8:], None
145 else:
146 # I can't work out how to create a test where the signature
147 # was expired at the time of signing.
148- return SIGNATURE_NOT_VALID, None
149+ return SIGNATURE_NOT_VALID, None, None
150
151 # GPG does not know this key.
152 # test_verify_unknown_key()
153 if error.result.signatures[0].summary & gpg.constants.SIGSUM_KEY_MISSING:
154- return SIGNATURE_KEY_MISSING, fingerprint[-8:]
155+ return SIGNATURE_KEY_MISSING, fingerprint[-8:], None
156
157- return SIGNATURE_NOT_VALID, None
158+ return SIGNATURE_NOT_VALID, None, None
159 except gpg.errors.GPGMEError as error:
160- raise SignatureVerificationFailed(error[2])
161+ raise SignatureVerificationFailed(error)
162
163 # No result if input is invalid.
164 # test_verify_invalid()
165 if len(result.signatures) == 0:
166- return SIGNATURE_NOT_VALID, None
167+ return SIGNATURE_NOT_VALID, None, plain_output
168+
169 # User has specified a list of acceptable keys, check our result is in
170 # it. test_verify_unacceptable_key()
171 fingerprint = result.signatures[0].fpr
172 if self.acceptable_keys is not None:
173 if not fingerprint in self.acceptable_keys:
174- return SIGNATURE_KEY_MISSING, fingerprint[-8:]
175- # Check the signature actually matches the testament.
176- # test_verify_bad_testament()
177- if testament != plain_output:
178- return SIGNATURE_NOT_VALID, None
179+ return SIGNATURE_KEY_MISSING, fingerprint[-8:], plain_output
180 # Yay gpg set the valid bit.
181 # Can't write a test for this one as you can't set a key to be
182 # trusted using gpg.
183@@ -298,20 +306,20 @@
184 key = self.context.get_key(fingerprint)
185 name = key.uids[0].name
186 email = key.uids[0].email
187- return SIGNATURE_VALID, name + " <" + email + ">"
188+ return SIGNATURE_VALID, name.decode('utf-8') + u" <" + email.decode('utf-8') + u">", plain_output
189 # Sigsum_red indicates a problem, unfortunatly I have not been able
190 # to write any tests which actually set this.
191 if result.signatures[0].summary & gpg.constants.SIGSUM_RED:
192- return SIGNATURE_NOT_VALID, None
193+ return SIGNATURE_NOT_VALID, None, plain_output
194 # Summary isn't set if sig is valid but key is untrusted but if user
195 # has explicity set the key as acceptable we can validate it.
196 if result.signatures[0].summary == 0 and self.acceptable_keys is not None:
197 if fingerprint in self.acceptable_keys:
198 # test_verify_untrusted_but_accepted()
199- return SIGNATURE_VALID, None
200+ return SIGNATURE_VALID, None, plain_output
201 # test_verify_valid_but_untrusted()
202 if result.signatures[0].summary == 0 and self.acceptable_keys is None:
203- return SIGNATURE_NOT_VALID, None
204+ return SIGNATURE_NOT_VALID, None, plain_output
205 # Other error types such as revoked keys should (I think) be caught by
206 # SIGSUM_RED so anything else means something is buggy.
207 raise SignatureVerificationFailed(
208
209=== modified file 'breezy/merge_directive.py'
210--- breezy/merge_directive.py 2017-10-27 00:18:42 +0000
211+++ breezy/merge_directive.py 2018-03-24 01:56:28 +0000
212@@ -251,7 +251,7 @@
213 :return: a string
214 """
215 my_gpg = gpg.GPGStrategy(branch.get_config_stack())
216- return my_gpg.sign(''.join(self.to_lines()))
217+ return my_gpg.sign(''.join(self.to_lines()), gpg.MODE_CLEAR)
218
219 def to_email(self, mail_to, branch, sign=False):
220 """Serialize as an email message.
221
222=== modified file 'breezy/repository.py'
223--- breezy/repository.py 2018-02-24 15:50:23 +0000
224+++ breezy/repository.py 2018-03-24 01:56:28 +0000
225@@ -887,7 +887,7 @@
226
227 def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
228 with self.lock_write():
229- signature = gpg_strategy.sign(plaintext)
230+ signature = gpg_strategy.sign(plaintext, gpg.MODE_CLEAR)
231 self.add_signature_text(revision_id, signature)
232
233 def add_signature_text(self, revision_id, signature):
234@@ -1104,11 +1104,12 @@
235 return gpg.SIGNATURE_NOT_SIGNED, None
236 signature = self.get_signature_text(revision_id)
237
238- testament = _mod_testament.Testament.from_revision(
239- self, revision_id)
240- plaintext = testament.as_short_text()
241+ testament = _mod_testament.Testament.from_revision(self, revision_id)
242
243- return gpg_strategy.verify(signature, plaintext)
244+ (status, key, signed_plaintext) = gpg_strategy.verify(signature)
245+ if testament.as_short_text() != signed_plaintext:
246+ return gpg.SIGNATURE_NOT_VALID, None
247+ return (status, key)
248
249 def verify_revision_signatures(self, revision_ids, gpg_strategy):
250 """Verify revision signatures for a number of revisions.
251
252=== modified file 'breezy/tests/per_repository/test_signatures.py'
253--- breezy/tests/per_repository/test_signatures.py 2017-08-07 19:09:47 +0000
254+++ breezy/tests/per_repository/test_signatures.py 2018-03-24 01:56:28 +0000
255@@ -123,7 +123,7 @@
256 '-----END PSEUDO-SIGNED CONTENT-----\n',
257 repo.get_signature_text(a))
258 self.assertEqual(
259- (gpg.SIGNATURE_VALID, None, ),
260+ (gpg.SIGNATURE_VALID, None),
261 repo.verify_revision_signature(a, strategy))
262
263 def test_verify_revision_signatures(self):
264
265=== modified file 'breezy/tests/test_commit.py'
266--- breezy/tests/test_commit.py 2018-02-15 19:38:33 +0000
267+++ breezy/tests/test_commit.py 2018-03-24 01:56:28 +0000
268@@ -435,7 +435,8 @@
269 message="base", allow_pointless=True, rev_id='B',
270 working_tree=wt)
271 def sign(text):
272- return breezy.gpg.LoopbackGPGStrategy(None).sign(text)
273+ return breezy.gpg.LoopbackGPGStrategy(None).sign(
274+ text, breezy.gpg.MODE_CLEAR)
275 self.assertEqual(sign(Testament.from_revision(branch.repository,
276 'B').as_short_text()),
277 branch.repository.get_signature_text('B'))
278
279=== modified file 'breezy/tests/test_gpg.py'
280--- breezy/tests/test_gpg.py 2017-07-04 20:03:11 +0000
281+++ breezy/tests/test_gpg.py 2018-03-24 01:56:28 +0000
282@@ -224,8 +224,7 @@
283 """
284 my_gpg = gpg.GPGStrategy(FakeConfig())
285 my_gpg.set_acceptable_keys("bazaar@example.com")
286- self.assertEqual((gpg.SIGNATURE_VALID, None), my_gpg.verify(content,
287- plain))
288+ self.assertEqual((gpg.SIGNATURE_VALID, None, plain), my_gpg.verify(content))
289
290 def test_verify_unacceptable_key(self):
291 self.requireFeature(features.gpg)
292@@ -255,8 +254,8 @@
293 """
294 my_gpg = gpg.GPGStrategy(FakeConfig())
295 my_gpg.set_acceptable_keys("foo@example.com")
296- self.assertEqual((gpg.SIGNATURE_KEY_MISSING, u'E3080E45'),
297- my_gpg.verify(content, plain))
298+ self.assertEqual((gpg.SIGNATURE_KEY_MISSING, u'E3080E45', plain),
299+ my_gpg.verify(content))
300
301 def test_verify_valid_but_untrusted(self):
302 self.requireFeature(features.gpg)
303@@ -285,40 +284,7 @@
304 sha1: 6411f9bdf6571200357140c9ce7c0f50106ac9a4
305 """
306 my_gpg = gpg.GPGStrategy(FakeConfig())
307- self.assertEqual((gpg.SIGNATURE_NOT_VALID, None), my_gpg.verify(content,
308- plain))
309-
310- def test_verify_bad_testament(self):
311- self.requireFeature(features.gpg)
312- self.import_keys()
313-
314- content = """-----BEGIN PGP SIGNED MESSAGE-----
315-Hash: SHA1
316-
317-bazaar-ng testament short form 1
318-revision-id: amy@example.com-20110527185938-hluafawphszb8dl1
319-sha1: 6411f9bdf6571200357140c9ce7c0f50106ac9a4
320------BEGIN PGP SIGNATURE-----
321-Version: GnuPG v1.4.11 (GNU/Linux)
322-
323-iQEcBAEBAgAGBQJN+ekFAAoJEIdoGx7jCA5FGtEH/i+XxJRvqU6wdBtLVrGBMAGk
324-FZ5VP+KyXYtymSbgSstj/vM12NeMIeFs3xGnNnYuX1MIcY6We5TKtCH0epY6ym5+
325-6g2Q2QpQ5/sT2d0mWzR0K4uVngmxVQaXTdk5PdZ40O7ULeDLW6CxzxMHyUL1rsIx
326-7UBUTBh1O/1n3ZfD99hUkm3hVcnsN90uTKH59zV9NWwArU0cug60+5eDKJhSJDbG
327-rIwlqbFAjDZ7L/48e+IaYIJwBZFzMBpJKdCxzALLtauMf+KK8hGiL2hrRbWm7ty6
328-NgxfkMYOB4rDPdSstT35N+5uBG3n/UzjxHssi0svMfVETYYX40y57dm2eZQXFp8=
329-=iwsn
330------END PGP SIGNATURE-----
331-"""
332- plain = """bazaar-ng testament short form 1
333-revision-id: doctor@example.com-20110527185938-hluafawphszb8dl1
334-sha1: 6411f9bdf6571200357140c9ce7c0f50106ac9a4
335-"""
336- my_gpg = gpg.GPGStrategy(FakeConfig())
337- my_gpg.set_acceptable_keys("bazaar@example.com")
338- self.assertEqual((gpg.SIGNATURE_NOT_VALID, None), my_gpg.verify(content,
339- plain))
340-
341+ self.assertEqual((gpg.SIGNATURE_NOT_VALID, None, plain), my_gpg.verify(content))
342
343 def test_verify_revoked_signature(self):
344 self.requireFeature(features.gpg)
345@@ -341,8 +307,7 @@
346 plain = """asdf\n"""
347 my_gpg = gpg.GPGStrategy(FakeConfig())
348 my_gpg.set_acceptable_keys("test@example.com")
349- self.assertEqual((gpg.SIGNATURE_NOT_VALID, None), my_gpg.verify(content,
350- plain))
351+ self.assertEqual((gpg.SIGNATURE_NOT_VALID, None, None), my_gpg.verify(content))
352
353 def test_verify_invalid(self):
354 self.requireFeature(features.gpg)
355@@ -366,8 +331,8 @@
356 sha1: 6411f9bdf6571200357140c9ce7c0f50106ac9a4
357 """
358 my_gpg = gpg.GPGStrategy(FakeConfig())
359- self.assertEqual((gpg.SIGNATURE_NOT_VALID, None),
360- my_gpg.verify(content, plain))
361+ self.assertEqual((gpg.SIGNATURE_NOT_VALID, None, plain),
362+ my_gpg.verify(content))
363
364 def test_verify_expired_but_valid(self):
365 self.requireFeature(features.gpg)
366@@ -388,13 +353,9 @@
367 =uHen
368 -----END PGP SIGNATURE-----
369 """
370- plain = """bazaar-ng testament short form 1
371-revision-id: test@example.com-20110801100657-f1dr1nompeex723z
372-sha1: 59ab434be4c2d5d646dee84f514aa09e1b72feeb
373-"""
374 my_gpg = gpg.GPGStrategy(FakeConfig())
375- self.assertEqual((gpg.SIGNATURE_EXPIRED, u'4F8D1513'),
376- my_gpg.verify(content, plain))
377+ self.assertEqual((gpg.SIGNATURE_EXPIRED, u'4F8D1513', None),
378+ my_gpg.verify(content))
379
380 def test_verify_unknown_key(self):
381 self.requireFeature(features.gpg)
382@@ -415,10 +376,9 @@
383 =RNR5
384 -----END PGP SIGNATURE-----
385 """
386- plain = "asdf\n"
387 my_gpg = gpg.GPGStrategy(FakeConfig())
388- self.assertEqual((gpg.SIGNATURE_KEY_MISSING, u'5D51E56F'),
389- my_gpg.verify(content, plain))
390+ self.assertEqual((gpg.SIGNATURE_KEY_MISSING, u'5D51E56F', None),
391+ my_gpg.verify(content))
392
393 def test_set_acceptable_keys(self):
394 self.requireFeature(features.gpg)
395@@ -454,9 +414,8 @@
396
397 def test_sign(self):
398 self.assertRaises(gpg.SigningFailed,
399- gpg.DisabledGPGStrategy(None).sign, 'content')
400+ gpg.DisabledGPGStrategy(None).sign, 'content', gpg.MODE_CLEAR)
401
402 def test_verify(self):
403 self.assertRaises(gpg.SignatureVerificationFailed,
404- gpg.DisabledGPGStrategy(None).verify, 'content',
405- 'testament')
406+ gpg.DisabledGPGStrategy(None).verify, 'content')
407
408=== modified file 'breezy/tests/test_log.py'
409--- breezy/tests/test_log.py 2017-12-04 00:25:02 +0000
410+++ breezy/tests/test_log.py 2018-03-24 01:56:28 +0000
411@@ -317,31 +317,22 @@
412
413
414 class TestFormatSignatureValidity(tests.TestCaseWithTransport):
415- class UTFLoopbackGPGStrategy(gpg.LoopbackGPGStrategy):
416- def verify(self, content, testament):
417- return (gpg.SIGNATURE_VALID,
418- u'UTF8 Test \xa1\xb1\xc1\xd1\xe1\xf1 <jrandom@example.com>')
419-
420- def has_signature_for_revision_id(self, revision_id):
421- return True
422-
423- def get_signature_text(self, revision_id):
424- return ''
425+
426+ def verify_revision_signature(self, revid, gpg_strategy):
427+ return (gpg.SIGNATURE_VALID,
428+ u'UTF8 Test \xa1\xb1\xc1\xd1\xe1\xf1 <jrandom@example.com>')
429
430 def test_format_signature_validity_utf(self):
431 """Check that GPG signatures containing UTF-8 names are formatted
432 correctly."""
433- # Monkey patch to use our UTF-8 generating GPGStrategy
434- self.overrideAttr(gpg, 'GPGStrategy', self.UTFLoopbackGPGStrategy)
435 wt = self.make_branch_and_tree('.')
436 revid = wt.commit('empty commit')
437 repo = wt.branch.repository
438 # Monkey patch out checking if this rev is actually signed, since we
439 # can't sign it without a heavier TestCase and LoopbackGPGStrategy
440 # doesn't care anyways.
441- self.overrideAttr(repo, 'has_signature_for_revision_id',
442- self.has_signature_for_revision_id)
443- self.overrideAttr(repo, 'get_signature_text', self.get_signature_text)
444+ self.overrideAttr(repo, 'verify_revision_signature',
445+ self.verify_revision_signature)
446 out = log.format_signature_validity(revid, wt.branch)
447 self.assertEqual(
448 u'valid signature from UTF8 Test \xa1\xb1\xc1\xd1\xe1\xf1 <jrandom@example.com>',

Subscribers

People subscribed via source and target branches