Merge lp:~cjwatson/launchpad/reject-bad-ssh-keys into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18565
Proposed branch: lp:~cjwatson/launchpad/reject-bad-ssh-keys
Merge into: lp:launchpad
Diff against target: 333 lines (+89/-44)
7 files modified
lib/lp/registry/browser/tests/test_person_webservice.py (+7/-6)
lib/lp/registry/browser/tests/test_sshkey.py (+2/-2)
lib/lp/registry/interfaces/ssh.py (+9/-6)
lib/lp/registry/model/person.py (+13/-3)
lib/lp/registry/tests/test_personset.py (+8/-7)
lib/lp/registry/tests/test_ssh.py (+20/-11)
lib/lp/testing/factory.py (+30/-9)
To merge this branch: bzr merge lp:~cjwatson/launchpad/reject-bad-ssh-keys
Reviewer Review Type Date Requested Status
Maximiliano Bertacchini (community) Approve
Launchpad code reviewers Pending
Review via email: mp+339445@code.launchpad.net

Commit message

Reject SSH public keys that Twisted can't load.

To post a comment you must log in.
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

Wow, a 10 year old issue! Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/tests/test_person_webservice.py'
2--- lib/lp/registry/browser/tests/test_person_webservice.py 2018-01-02 16:10:26 +0000
3+++ lib/lp/registry/browser/tests/test_person_webservice.py 2018-02-24 09:42:40 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 __metaclass__ = type
10@@ -562,21 +562,22 @@
11 with admin_logged_in():
12 person = removeSecurityProxy(self.factory.makePerson())
13 openid_id = person.account.openid_identifiers.any().identifier
14- response = self.addSSHKeyForPerson(
15- openid_id, 'ssh-rsa keydata comment')
16+ full_key = self.factory.makeSSHKeyText()
17+ _, keytext, comment = full_key.split(' ', 2)
18+ response = self.addSSHKeyForPerson(openid_id, full_key)
19
20 self.assertEqual(200, response.status)
21 [key] = person.sshkeys
22 self.assertEqual(SSHKeyType.RSA, key.keytype)
23- self.assertEqual('keydata', key.keytext)
24- self.assertEqual('comment', key.comment)
25+ self.assertEqual(keytext, key.keytext)
26+ self.assertEqual(comment, key.comment)
27
28 def test_addSSHKeyFromSSO_dry_run(self):
29 with admin_logged_in():
30 person = removeSecurityProxy(self.factory.makePerson())
31 openid_id = person.account.openid_identifiers.any().identifier
32 response = self.addSSHKeyForPerson(
33- openid_id, 'ssh-rsa keydata comment', dry_run=True)
34+ openid_id, self.factory.makeSSHKeyText(), dry_run=True)
35
36 self.assertEqual(200, response.status)
37 self.assertEqual(0, person.sshkeys.count())
38
39=== modified file 'lib/lp/registry/browser/tests/test_sshkey.py'
40--- lib/lp/registry/browser/tests/test_sshkey.py 2017-06-01 12:58:53 +0000
41+++ lib/lp/registry/browser/tests/test_sshkey.py 2018-02-24 09:42:40 +0000
42@@ -1,4 +1,4 @@
43-# Copyright 2010-2017 Canonical Ltd. This software is licensed under the
44+# Copyright 2010-2018 Canonical Ltd. This software is licensed under the
45 # GNU Affero General Public License version 3 (see the file LICENSE).
46
47 """Tests for GPG key on the web."""
48@@ -51,7 +51,7 @@
49 with person_logged_in(person):
50 # Add the key for the user here,
51 # since we only care about testing removal.
52- getUtility(ISSHKeySet).new(person, public_key)
53+ getUtility(ISSHKeySet).new(person, public_key, check_key=False)
54 browser = setupBrowserFreshLogin(person)
55 browser.open(url)
56 browser.getControl('Remove').click()
57
58=== modified file 'lib/lp/registry/interfaces/ssh.py'
59--- lib/lp/registry/interfaces/ssh.py 2017-06-01 12:58:53 +0000
60+++ lib/lp/registry/interfaces/ssh.py 2018-02-24 09:42:40 +0000
61@@ -1,4 +1,4 @@
62-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
63+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
64 # GNU Affero General Public License version 3 (see the file LICENSE).
65
66 """SSH key interfaces."""
67@@ -89,16 +89,19 @@
68 class ISSHKeySet(Interface):
69 """The set of SSHKeys."""
70
71- def new(person, sshkey, send_notification=True, dry_run=False):
72+ def new(person, sshkey, check_key=True, send_notification=True,
73+ dry_run=False):
74 """Create a new SSHKey pointing to the given Person.
75
76 :param person: The IPerson to add the ssh key to.
77 :param sshkey: The full ssh key text.
78- :param send_notification: Set to False to supress sending the user an
79+ :param check_key: Set to False to skip the check for whether the key
80+ text can be deserialised by Twisted.
81+ :param send_notification: Set to False to suppress sending the user an
82 email about the change.
83- :param dry_run: Perform all the format and vaulnerability checks, but
84- don't actually add the key. Causes the method to return None,
85- rather than an instance of ISSHKey.
86+ :param dry_run: Perform all the format checks, but don't actually
87+ add the key. Causes the method to return None, rather than an
88+ instance of ISSHKey.
89 """
90
91 def getByID(id, default=None):
92
93=== modified file 'lib/lp/registry/model/person.py'
94--- lib/lp/registry/model/person.py 2017-09-28 13:06:10 +0000
95+++ lib/lp/registry/model/person.py 2018-02-24 09:42:40 +0000
96@@ -1,4 +1,4 @@
97-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
98+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
99 # GNU Affero General Public License version 3 (see the file LICENSE).
100
101 """Implementation classes for a Person."""
102@@ -81,6 +81,7 @@
103 Store,
104 )
105 import transaction
106+from twisted.conch.ssh.keys import Key
107 from zope.component import (
108 adapter,
109 getUtility,
110@@ -3512,7 +3513,8 @@
111 raise NoSuchAccount("No account found for openid identifier '%s'"
112 % openid_identifier)
113 getUtility(ISSHKeySet).new(
114- IPerson(account), key_text, False, dry_run=dry_run)
115+ IPerson(account), key_text, send_notification=False,
116+ dry_run=dry_run)
117
118 def deleteSSHKeyFromSSO(self, user, openid_identifier, key_text,
119 dry_run=False):
120@@ -4123,9 +4125,17 @@
121 @implementer(ISSHKeySet)
122 class SSHKeySet:
123
124- def new(self, person, sshkey, send_notification=True, dry_run=False):
125+ def new(self, person, sshkey, check_key=True, send_notification=True,
126+ dry_run=False):
127 keytype, keytext, comment = self._extract_ssh_key_components(sshkey)
128
129+ if check_key:
130+ try:
131+ Key.fromString(sshkey)
132+ except Exception as e:
133+ raise SSHKeyAdditionError(
134+ "Invalid SSH key data: '%s' (%s)" % (sshkey, e))
135+
136 if send_notification:
137 person.security_field_changed(
138 "New SSH key added to your account.",
139
140=== modified file 'lib/lp/registry/tests/test_personset.py'
141--- lib/lp/registry/tests/test_personset.py 2018-01-02 16:10:26 +0000
142+++ lib/lp/registry/tests/test_personset.py 2018-02-24 09:42:40 +0000
143@@ -1,4 +1,4 @@
144-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
145+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
146 # GNU Affero General Public License version 3 (see the file LICENSE).
147
148 """Tests for PersonSet."""
149@@ -976,7 +976,7 @@
150 def test_restricted_to_sso(self):
151 # Only the ubuntu-sso celebrity can invoke this
152 # privileged method.
153- key_text = 'ssh-rsa keytext keycomment'
154+ key_text = self.factory.makeSSHKeyText()
155 target = self.factory.makePerson(name='username')
156 make_openid_identifier(target.account, u'openid')
157
158@@ -998,22 +998,23 @@
159 self.assertEqual(None, do_it())
160
161 def test_adds_new_ssh_key(self):
162- key_text = 'ssh-rsa keytext keycomment'
163+ full_key = self.factory.makeSSHKeyText()
164+ _, keytext, comment = full_key.split(' ', 2)
165 target = self.factory.makePerson(name='username')
166 make_openid_identifier(target.account, u'openid')
167
168 with person_logged_in(self.sso):
169 getUtility(IPersonSet).addSSHKeyFromSSO(
170- self.sso, u'openid', key_text, False)
171+ self.sso, u'openid', full_key, False)
172
173 with person_logged_in(target):
174 [key] = target.sshkeys
175 self.assertEqual(key.keytype, SSHKeyType.RSA)
176- self.assertEqual(key.keytext, 'keytext')
177- self.assertEqual(key.comment, 'keycomment')
178+ self.assertEqual(key.keytext, keytext)
179+ self.assertEqual(key.comment, comment)
180
181 def test_does_not_add_new_ssh_key_with_dry_run(self):
182- key_text = 'ssh-rsa keytext keycomment'
183+ key_text = self.factory.makeSSHKeyText()
184 target = self.factory.makePerson(name='username')
185 make_openid_identifier(target.account, u'openid')
186
187
188=== modified file 'lib/lp/registry/tests/test_ssh.py'
189--- lib/lp/registry/tests/test_ssh.py 2017-06-01 12:58:53 +0000
190+++ lib/lp/registry/tests/test_ssh.py 2018-02-24 09:42:40 +0000
191@@ -1,4 +1,4 @@
192-# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
193+# Copyright 2016-2018 Canonical Ltd. This software is licensed under the
194 # GNU Affero General Public License version 3 (see the file LICENSE).
195
196 """Tests for SSHKey."""
197@@ -74,9 +74,10 @@
198
199 def test_sends_notification_by_default(self):
200 person = self.factory.makePerson()
201+ key_text = self.factory.makeSSHKeyText(comment="comment")
202 with person_logged_in(person):
203 keyset = getUtility(ISSHKeySet)
204- keyset.new(person, "ssh-rsa keytext comment")
205+ keyset.new(person, key_text)
206 [email] = pop_notifications()
207 self.assertEqual(
208 email['Subject'], "New SSH key added to your account.")
209@@ -89,7 +90,7 @@
210 person = self.factory.makePerson()
211 with person_logged_in(person):
212 keyset = getUtility(ISSHKeySet)
213- keyset.new(person, "ssh-rsa keytext comment",
214+ keyset.new(person, self.factory.makeSSHKeyText(),
215 send_notification=False)
216 self.assertEqual([], pop_notifications())
217
218@@ -138,10 +139,8 @@
219 def test_can_add_new_key(self):
220 keyset = getUtility(ISSHKeySet)
221 person = self.factory.makePerson()
222- keytype = 'ssh-rsa'
223- keytext = 'ThisIsAFakeSSHKey'
224- comment = 'This is a key comment.'
225- full_key = ' '.join((keytype, keytext, comment))
226+ full_key = self.factory.makeSSHKeyText()
227+ keytype, keytext, comment = full_key.split(' ', 2)
228 with person_logged_in(person):
229 key = keyset.new(person, full_key)
230 self.assertEqual([key], list(person.sshkeys))
231@@ -152,18 +151,28 @@
232 def test_new_raises_KeyAdditionError_on_bad_key_data(self):
233 person = self.factory.makePerson()
234 keyset = getUtility(ISSHKeySet)
235- self.assertRaises(
236+ self.assertRaisesWithContent(
237 SSHKeyAdditionError,
238+ "Invalid SSH key data: 'thiskeyhasnospaces'",
239 keyset.new,
240 person, 'thiskeyhasnospaces'
241 )
242- self.assertRaises(
243+ self.assertRaisesWithContent(
244 SSHKeyAdditionError,
245+ "Invalid SSH key type: 'bad_key_type'",
246 keyset.new,
247 person, 'bad_key_type keytext comment'
248 )
249- self.assertRaises(
250- SSHKeyAdditionError,
251+ self.assertRaisesWithContent(
252+ SSHKeyAdditionError,
253+ "Invalid SSH key data: 'ssh-rsa badkeytext comment' "
254+ "(Incorrect padding)",
255+ keyset.new,
256+ person, 'ssh-rsa badkeytext comment'
257+ )
258+ self.assertRaisesWithContent(
259+ SSHKeyAdditionError,
260+ "Invalid SSH key data: 'None'",
261 keyset.new,
262 person, None
263 )
264
265=== modified file 'lib/lp/testing/factory.py'
266--- lib/lp/testing/factory.py 2018-02-02 10:06:24 +0000
267+++ lib/lp/testing/factory.py 2018-02-24 09:42:40 +0000
268@@ -19,6 +19,7 @@
269 'remove_security_proxy_and_shout_at_engineer',
270 ]
271
272+import base64
273 from datetime import (
274 datetime,
275 timedelta,
276@@ -50,6 +51,11 @@
277 from lazr.jobrunner.jobrunner import SuspendJobException
278 import pytz
279 from pytz import UTC
280+from twisted.conch.ssh.common import (
281+ MP,
282+ NS,
283+ )
284+from twisted.conch.test import keydata
285 from twisted.python.util import mergeFunctionMetadata
286 from zope.component import (
287 ComponentLookupError,
288@@ -4304,6 +4310,29 @@
289 return getUtility(IHWSubmissionDeviceSet).create(
290 device_driver_link, submission, parent, hal_device_id)
291
292+ def makeSSHKeyText(self, key_type=SSHKeyType.RSA, comment=None):
293+ """Create new SSH public key text.
294+
295+ :param key_type: If specified, the type of SSH key to generate. Must be
296+ a member of SSHKeyType. If unspecified, SSHKeyType.RSA is used.
297+ """
298+ key_type_string = SSH_KEY_TYPE_TO_TEXT.get(key_type)
299+ if key_type is None:
300+ raise AssertionError(
301+ "key_type must be a member of SSHKeyType, not %r" % key_type)
302+ if key_type == SSHKeyType.RSA:
303+ parameters = [MP(keydata.RSAData[param]) for param in ("e", "n")]
304+ elif key_type == SSHKeyType.DSA:
305+ parameters = [
306+ MP(keydata.DSAData[param]) for param in ("p", "q", "g", "y")]
307+ else:
308+ raise AssertionError(
309+ "key_type must be a member of SSHKeyType, not %r" % key_type)
310+ key_text = base64.b64encode(NS(key_type_string) + b"".join(parameters))
311+ if comment is None:
312+ comment = self.getUniqueString()
313+ return "%s %s %s" % (key_type_string, key_text, comment)
314+
315 def makeSSHKey(self, person=None, key_type=SSHKeyType.RSA,
316 send_notification=True):
317 """Create a new SSHKey.
318@@ -4315,15 +4344,7 @@
319 """
320 if person is None:
321 person = self.makePerson()
322- key_type_string = SSH_KEY_TYPE_TO_TEXT.get(key_type)
323- if key_type is None:
324- raise AssertionError(
325- "key_type must be a member of SSHKeyType, not %r" % key_type)
326- public_key = "%s %s %s" % (
327- key_type_string,
328- self.getUniqueString(),
329- self.getUniqueString(),
330- )
331+ public_key = self.makeSSHKeyText(key_type=key_type)
332 return getUtility(ISSHKeySet).new(
333 person, public_key, send_notification=send_notification)
334