Merge lp:~vexo/bzr/fix-1123460 into lp:bzr

Proposed by Reagan Sanders
Status: Merged
Approved by: Richard Wilbur
Approved revision: no longer in the source branch.
Merged at revision: 6594
Proposed branch: lp:~vexo/bzr/fix-1123460
Merge into: lp:bzr
Diff against target: 277 lines (+162/-45)
6 files modified
bzrlib/commit_signature_commands.py (+2/-1)
bzrlib/log.py (+1/-1)
bzrlib/tests/blackbox/__init__.py (+1/-0)
bzrlib/tests/blackbox/test_sign_my_commits.py (+0/-43)
bzrlib/tests/blackbox/test_verify_signatures.py (+124/-0)
bzrlib/tests/test_log.py (+34/-0)
To merge this branch: bzr merge lp:~vexo/bzr/fix-1123460
Reviewer Review Type Date Requested Status
Reagan Sanders (community) Approve
Richard Wilbur Approve
Review via email: mp+177290@code.launchpad.net

Commit message

Fix bug LP: #1123460, verify-signature crashes on non ascii characters (and on -v)

Description of the change

Fixes bug LP: #1123460. This bug really wraps up two completely independent issues.

First, the issue with verify-signatures --verbose. We were attempting to pass an array of strings to write() directly. Changed to loop through the array and write them all.

Second, issue with UTF characters in GPG names. We were trying to stick the UTF strings from the GPG subsystem into plain ASCII format strings. Changed the format strings to be UTF as well.

After these fixes, bzr verify-signatures -v and bzr qlog both succeed on a test repository signed with a key containing a UTF-8 name.

vexo@anput:~/sign$ bzr verify-signatures -v
1 commits with valid signatures
  ÀÇÐőbˆ Doe <email address hidden> signed 1 commit
0 commits with key now expired
1 commit with unknown key
  Unknown key B2C03A8B signed 1 commit
0 commits not valid
0 commits not signed
vexo@anput:~/sign$ bzr qlog
vexo@anput:~/sign$

To post a comment you must log in.
Revision history for this message
Reagan Sanders (vexo) wrote :

Additional test example for the all-valid code path:

vexo@anput:~/sign$ bzr verify-signatures -v
All commits signed with verifiable keys
  ÀÇÐőbˆ Doe <email address hidden> signed 1 commit
vexo@anput:~/sign$

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

On Sun, Jul 28, 2013 at 12:06 PM, Reagan Sanders <email address hidden> wrote:
> Additional test example for the all-valid code path:
>
> vexo@anput:~/sign$ bzr verify-signatures -v
> All commits signed with verifiable keys
> ÀÇÐőbˆ Doe <email address hidden> signed 1 commit
> vexo@anput:~/sign$
> --
> https://code.launchpad.net/~vexofp/bzr/fix-1123460/+merge/177290
> Your team bzr-core is requested to review the proposed merge of lp:~vexofp/bzr/fix-1123460 into lp:bzr.

Reagan,

I tried to review this merge request and I'm having trouble bringing
it up. It looks like some great fixes that would be nice to get in
the trunk.

I don't think the change to write out the array of strings in a loop
requires a test case--you fixed a flaw in the implementation! Nice
work.

How hard would it be to code up your 'verify signatures' test case?

Sincerely,

Richard

Revision history for this message
Reagan Sanders (vexo) wrote :

I went ahead and added a couple basic test cases for verify-signatures --verbose, as it was completely uncovered previously and that didn't seem good. The minimal existing tests for verify-signatures were also oddly lumped in with sign-my-commits, so I split everything off into a new file.

After looking at it a bit more, the UTF-8 issue never actually had anything to do with verify-signatures, and the issue was really with generating the log, so I added the test case for it there. It seems like there might be a cleaner way of writing the test, but actually signing the test commit didn't seem to play nice with the way TestCaseWithTransport sets up its repository.

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

Thanks for the great work! I sincerely appreciate the improved test coverage, and thank you for splitting out the verify-signatures test cases.
+1

review: Approve
Revision history for this message
Reagan Sanders (vexo) wrote :

Don't know how I overlooked the merge request for lp:~krause/bzr/1037108, but it appears to be a subset of what I've done here. I'm not sure if there's merge-proposal equivalent for marking it as a duplicate or invalid, but noting it here so it can be handled appropriately if this gets landed.

https://code.launchpad.net/~krause/bzr/1037108/+merge/119912

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

Thanks for noting the previous merge request. I will look into
marking it "duplicate" or "invalid", etc. Did you notice John
Meinel's suggestion to decode the utf-8 to unicode? Does that still
apply? I'm still learning about unicode vs. utf-8, etc.

Do we need to worry about unicode format string for
bzrlib/log.py:format_signature_validity:339 ? How about translations
of these format strings and the two format strings in
bzrlib/log.py:format_signature_validity:341,343?

Revision history for this message
Reagan Sanders (vexo) wrote :

> Thanks for noting the previous merge request. I will look into
> marking it "duplicate" or "invalid", etc. Did you notice John
> Meinel's suggestion to decode the utf-8 to unicode? Does that still
> apply? I'm still learning about unicode vs. utf-8, etc.

I don't *think* so, as pygpgme was already nice enough to do what John was suggesting might be required.

result[1] is created at gpg.py:295 as the concatenation of key.uids[0].name and key.uids[0].email, where key is self.context.get_key(fingerprint) as returned from pygpgme. Digging into the code for pygpgme, these equate to the c functions pygpgme_user_id_get_name/email, both of which return the output of a call to PyUnicode_DecodeUTF8. Thus, we already have a decoded python unicode object (vice a raw UTF-8 encoded bytestream), and don't need to decode it any further.

http://bazaar.launchpad.net/~jamesh/pygpgme/trunk/view/head:/src/pygpgme-key.c#L338

> Do we need to worry about unicode format string for
> bzrlib/log.py:format_signature_validity:339 ?

No, if the key is missing, that string will always be the hexadecimal ID for the string (eg. 1F4AAD09), since that would be the only thing we know at that point. Similar to the above analysis, the value here is derived from a call to PyUnicode_DecodeASCII in pygpgme, so this will always be a plain str object.

http://bazaar.launchpad.net/~jamesh/pygpgme/trunk/view/head:/src/pygpgme-key.c#L109

> How about translations
> of these format strings and the two format strings in
> bzrlib/log.py:format_signature_validity:341,343?

As far as I can tell, nothing else that shows up in the *output* of the log seems to be translated, though the error messages are. Either way, I think that'd go beyond the scope of this bug fix and into adding new functionality.

Now, a valid question might be why format_signature_validity is public at the module scope at all, rather than being a member of _DefaultLogGenerator like _format_diff? I don't see it being called anywhere else and can't think of much reason that it ever would be. Then again, there's a lot of public functions like that in log.py and I can't think of any specific downsides to exposing it.

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

I reviewed https://code.launchpad.net/~krause/bzr/1037108/+merge/119912 as "Disapprove (branch superceded)" with a pointer to this branch and a short explanation of the situation, then I marked the merge request "Rejected".

I think this ready for merging. Don't you?

Revision history for this message
Reagan Sanders (vexo) wrote :

Agree. (Good thing too, with how my free time has been lately... :/ )

review: Approve
Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

sent to pqm by email

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

sent to pqm by email

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

Reagan, I finally got my bzr and gpg configuration properly setup and the folks on launchpad setup the authentication configuration for pqm so I am able to merge to trunk. Many thanks to John Meinel and Vincent Ladeuil for their advice and assistance.

Thus, this branch finally landed on trunk!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/commit_signature_commands.py'
--- bzrlib/commit_signature_commands.py 2012-03-11 16:51:49 +0000
+++ bzrlib/commit_signature_commands.py 2013-08-11 20:38:13 +0000
@@ -165,7 +165,8 @@
165 if all_verifiable:165 if all_verifiable:
166 write(gettext("All commits signed with verifiable keys"))166 write(gettext("All commits signed with verifiable keys"))
167 if verbose:167 if verbose:
168 write(gpg.verbose_valid_message(result))168 for message in gpg.verbose_valid_message(result):
169 write_verbose(message)
169 return 0170 return 0
170 else:171 else:
171 write(gpg.valid_commits_message(count))172 write(gpg.valid_commits_message(count))
172173
=== modified file 'bzrlib/log.py'
--- bzrlib/log.py 2012-03-14 11:30:42 +0000
+++ bzrlib/log.py 2013-08-11 20:38:13 +0000
@@ -334,7 +334,7 @@
334 gpg_strategy = gpg.GPGStrategy(None)334 gpg_strategy = gpg.GPGStrategy(None)
335 result = repo.verify_revision_signature(rev_id, gpg_strategy)335 result = repo.verify_revision_signature(rev_id, gpg_strategy)
336 if result[0] == gpg.SIGNATURE_VALID:336 if result[0] == gpg.SIGNATURE_VALID:
337 return "valid signature from {0}".format(result[1])337 return u"valid signature from {0}".format(result[1])
338 if result[0] == gpg.SIGNATURE_KEY_MISSING:338 if result[0] == gpg.SIGNATURE_KEY_MISSING:
339 return "unknown key {0}".format(result[1])339 return "unknown key {0}".format(result[1])
340 if result[0] == gpg.SIGNATURE_NOT_VALID:340 if result[0] == gpg.SIGNATURE_NOT_VALID:
341341
=== modified file 'bzrlib/tests/blackbox/__init__.py'
--- bzrlib/tests/blackbox/__init__.py 2012-09-08 13:26:18 +0000
+++ bzrlib/tests/blackbox/__init__.py 2013-08-11 20:38:13 +0000
@@ -118,6 +118,7 @@
118 'test_shell_complete',118 'test_shell_complete',
119 'test_shelve',119 'test_shelve',
120 'test_sign_my_commits',120 'test_sign_my_commits',
121 'test_verify_signatures',
121 'test_split',122 'test_split',
122 'test_status',123 'test_status',
123 'test_switch',124 'test_switch',
124125
=== modified file 'bzrlib/tests/blackbox/test_sign_my_commits.py'
--- bzrlib/tests/blackbox/test_sign_my_commits.py 2012-03-11 16:51:49 +0000
+++ bzrlib/tests/blackbox/test_sign_my_commits.py 2013-08-11 20:38:13 +0000
@@ -116,29 +116,6 @@
116 self.assertUnsigned(repo, 'D')116 self.assertUnsigned(repo, 'D')
117 self.assertUnsigned(repo, 'E')117 self.assertUnsigned(repo, 'E')
118118
119 def test_verify_commits(self):
120 wt = self.setup_tree()
121 self.monkey_patch_gpg()
122 self.run_bzr('sign-my-commits')
123 out = self.run_bzr('verify-signatures', retcode=1)
124 self.assertEquals(('4 commits with valid signatures\n'
125 '0 commits with key now expired\n'
126 '0 commits with unknown keys\n'
127 '0 commits not valid\n'
128 '1 commit not signed\n', ''), out)
129
130 def test_verify_commits_acceptable_key(self):
131 wt = self.setup_tree()
132 self.monkey_patch_gpg()
133 self.run_bzr('sign-my-commits')
134 out = self.run_bzr(['verify-signatures', '--acceptable-keys=foo,bar'],
135 retcode=1)
136 self.assertEquals(('4 commits with valid signatures\n'
137 '0 commits with key now expired\n'
138 '0 commits with unknown keys\n'
139 '0 commits not valid\n'
140 '1 commit not signed\n', ''), out)
141
142119
143class TestSmartServerSignMyCommits(tests.TestCaseWithTransport):120class TestSmartServerSignMyCommits(tests.TestCaseWithTransport):
144121
@@ -168,23 +145,3 @@
168 self.assertLength(15, self.hpss_calls)145 self.assertLength(15, self.hpss_calls)
169 self.assertLength(1, self.hpss_connections)146 self.assertLength(1, self.hpss_connections)
170 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)147 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
171
172 def test_verify_commits(self):
173 self.setup_smart_server_with_call_log()
174 t = self.make_branch_and_tree('branch')
175 self.build_tree_contents([('branch/foo', 'thecontents')])
176 t.add("foo")
177 t.commit("message")
178 self.monkey_patch_gpg()
179 out, err = self.run_bzr(['sign-my-commits', self.get_url('branch')])
180 self.reset_smart_call_log()
181 self.run_bzr('sign-my-commits')
182 out = self.run_bzr(['verify-signatures', self.get_url('branch')])
183 # This figure represent the amount of work to perform this use case. It
184 # is entirely ok to reduce this number if a test fails due to rpc_count
185 # being too low. If rpc_count increases, more network roundtrips have
186 # become necessary for this use case. Please do not adjust this number
187 # upwards without agreement from bzr's network support maintainers.
188 self.assertLength(10, self.hpss_calls)
189 self.assertLength(1, self.hpss_connections)
190 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
191148
=== added file 'bzrlib/tests/blackbox/test_verify_signatures.py'
--- bzrlib/tests/blackbox/test_verify_signatures.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/blackbox/test_verify_signatures.py 2013-08-11 20:38:13 +0000
@@ -0,0 +1,124 @@
1# Copyright (C) 2005 Canonical Ltd
2# -*- coding: utf-8 -*-
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
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
17
18"""Black-box tests for bzr verify-signatures."""
19
20from bzrlib import (
21 gpg,
22 tests,
23 )
24from bzrlib.tests.matchers import ContainsNoVfsCalls
25
26
27class TestVerifySignatures(tests.TestCaseWithTransport):
28
29 def monkey_patch_gpg(self):
30 """Monkey patch the gpg signing strategy to be a loopback.
31
32 This also registers the cleanup, so that we will revert to
33 the original gpg strategy when done.
34 """
35 # monkey patch gpg signing mechanism
36 self.overrideAttr(gpg, 'GPGStrategy', gpg.LoopbackGPGStrategy)
37
38 def setup_tree(self, location='.'):
39 wt = self.make_branch_and_tree(location)
40 wt.commit("base A", allow_pointless=True, rev_id='A')
41 wt.commit("base B", allow_pointless=True, rev_id='B')
42 wt.commit("base C", allow_pointless=True, rev_id='C')
43 wt.commit("base D", allow_pointless=True, rev_id='D',
44 committer='Alternate <alt@foo.com>')
45 wt.add_parent_tree_id("aghost")
46 wt.commit("base E", allow_pointless=True, rev_id='E')
47 return wt
48
49 def test_verify_signatures(self):
50 wt = self.setup_tree()
51 self.monkey_patch_gpg()
52 self.run_bzr('sign-my-commits')
53 out = self.run_bzr('verify-signatures', retcode=1)
54 self.assertEquals(('4 commits with valid signatures\n'
55 '0 commits with key now expired\n'
56 '0 commits with unknown keys\n'
57 '0 commits not valid\n'
58 '1 commit not signed\n', ''), out)
59
60 def test_verify_signatures_acceptable_key(self):
61 wt = self.setup_tree()
62 self.monkey_patch_gpg()
63 self.run_bzr('sign-my-commits')
64 out = self.run_bzr(['verify-signatures', '--acceptable-keys=foo,bar'],
65 retcode=1)
66 self.assertEquals(('4 commits with valid signatures\n'
67 '0 commits with key now expired\n'
68 '0 commits with unknown keys\n'
69 '0 commits not valid\n'
70 '1 commit not signed\n', ''), out)
71
72 def test_verify_signatures_verbose(self):
73 wt = self.setup_tree()
74 self.monkey_patch_gpg()
75 self.run_bzr('sign-my-commits')
76 out = self.run_bzr('verify-signatures --verbose', retcode=1)
77 self.assertEquals(('4 commits with valid signatures\n'
78 ' None signed 4 commits\n'
79 '0 commits with key now expired\n'
80 '0 commits with unknown keys\n'
81 '0 commits not valid\n'
82 '1 commit not signed\n'
83 ' 1 commit by author Alternate <alt@foo.com>\n', ''), out)
84
85 def test_verify_signatures_verbose_all_valid(self):
86 wt = self.setup_tree()
87 self.monkey_patch_gpg()
88 self.run_bzr('sign-my-commits')
89 self.run_bzr(['sign-my-commits', '.', 'Alternate <alt@foo.com>'])
90 out = self.run_bzr('verify-signatures --verbose')
91 self.assertEquals(('All commits signed with verifiable keys\n'
92 ' None signed 5 commits\n', ''), out)
93
94
95class TestSmartServerVerifySignatures(tests.TestCaseWithTransport):
96
97 def monkey_patch_gpg(self):
98 """Monkey patch the gpg signing strategy to be a loopback.
99
100 This also registers the cleanup, so that we will revert to
101 the original gpg strategy when done.
102 """
103 # monkey patch gpg signing mechanism
104 self.overrideAttr(gpg, 'GPGStrategy', gpg.LoopbackGPGStrategy)
105
106 def test_verify_signatures(self):
107 self.setup_smart_server_with_call_log()
108 t = self.make_branch_and_tree('branch')
109 self.build_tree_contents([('branch/foo', 'thecontents')])
110 t.add("foo")
111 t.commit("message")
112 self.monkey_patch_gpg()
113 out, err = self.run_bzr(['sign-my-commits', self.get_url('branch')])
114 self.reset_smart_call_log()
115 self.run_bzr('sign-my-commits')
116 out = self.run_bzr(['verify-signatures', self.get_url('branch')])
117 # This figure represent the amount of work to perform this use case. It
118 # is entirely ok to reduce this number if a test fails due to rpc_count
119 # being too low. If rpc_count increases, more network roundtrips have
120 # become necessary for this use case. Please do not adjust this number
121 # upwards without agreement from bzr's network support maintainers.
122 self.assertLength(10, self.hpss_calls)
123 self.assertLength(1, self.hpss_connections)
124 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
0125
=== modified file 'bzrlib/tests/test_log.py'
--- bzrlib/tests/test_log.py 2012-08-04 14:27:47 +0000
+++ bzrlib/tests/test_log.py 2013-08-11 20:38:13 +0000
@@ -25,6 +25,8 @@
25 revision,25 revision,
26 revisionspec,26 revisionspec,
27 tests,27 tests,
28 gpg,
29 trace,
28 )30 )
2931
3032
@@ -281,6 +283,38 @@
281 self.checkDelta(logentry.delta, added=['file1', 'file2'])283 self.checkDelta(logentry.delta, added=['file1', 'file2'])
282284
283285
286class TestFormatSignatureValidity(tests.TestCaseWithTransport):
287 class UTFLoopbackGPGStrategy(gpg.LoopbackGPGStrategy):
288 def verify(self, content, testament):
289 return (gpg.SIGNATURE_VALID,
290 u'UTF8 Test \xa1\xb1\xc1\xd1\xe1\xf1 <jrandom@example.com>')
291
292 def has_signature_for_revision_id(self, revision_id):
293 return True
294
295 def get_signature_text(self, revision_id):
296 return ''
297
298 def test_format_signature_validity_utf(self):
299 """Check that GPG signatures containing UTF-8 names are formatted
300 correctly."""
301 # Monkey patch to use our UTF-8 generating GPGStrategy
302 self.overrideAttr(gpg, 'GPGStrategy', self.UTFLoopbackGPGStrategy)
303 wt = self.make_branch_and_tree('.')
304 revid = wt.commit('empty commit')
305 repo = wt.branch.repository
306 # Monkey patch out checking if this rev is actually signed, since we
307 # can't sign it without a heavier TestCase and LoopbackGPGStrategy
308 # doesn't care anyways.
309 self.overrideAttr(repo, 'has_signature_for_revision_id',
310 self.has_signature_for_revision_id)
311 self.overrideAttr(repo, 'get_signature_text', self.get_signature_text)
312 out = log.format_signature_validity(revid, repo)
313 self.assertEqual(
314u'valid signature from UTF8 Test \xa1\xb1\xc1\xd1\xe1\xf1 <jrandom@example.com>',
315 out)
316
317
284class TestShortLogFormatter(TestCaseForLogFormatter):318class TestShortLogFormatter(TestCaseForLogFormatter):
285319
286 def test_trailing_newlines(self):320 def test_trailing_newlines(self):