Merge lp:~cjwatson/launchpad/ssh-ecdsa-keys into lp:launchpad
- ssh-ecdsa-keys
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18721 | ||||
Proposed branch: | lp:~cjwatson/launchpad/ssh-ecdsa-keys | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/lazr.sshserver-0.1.8 | ||||
Diff against target: |
449 lines (+152/-56) 7 files modified
lib/lp/registry/interfaces/ssh.py (+20/-10) lib/lp/registry/model/person.py (+26/-7) lib/lp/registry/scripts/listteammembers.py (+3/-8) lib/lp/registry/stories/person/xx-add-sshkey.txt (+32/-5) lib/lp/registry/templates/person-editsshkeys.pt (+2/-1) lib/lp/registry/tests/test_ssh.py (+32/-4) lib/lp/testing/factory.py (+37/-21) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/ssh-ecdsa-keys | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+348848@code.launchpad.net |
Commit message
Add support for SSH ECDSA keys.
Description of the change
The main difficulty is that there are multiple public key algorithm names for ECDSA, so we need to stop having data structures laid out on the assumption that there's a bijection between public key algorithm names and SSHKeyType enumeration items. (The alternative was to have one SSHKeyType item for each curve size, but that seemed a bit silly.)
The prerequisite branch and the corresponding branches of txpkgupload and turnip must all be deployed to production before this is landed, to avoid the situation where people can upload keys they can't use.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/registry/interfaces/ssh.py' | |||
2 | --- lib/lp/registry/interfaces/ssh.py 2018-06-25 15:14:38 +0000 | |||
3 | +++ lib/lp/registry/interfaces/ssh.py 2018-07-02 14:38:50 +0000 | |||
4 | @@ -8,7 +8,6 @@ | |||
5 | 8 | __all__ = [ | 8 | __all__ = [ |
6 | 9 | 'ISSHKey', | 9 | 'ISSHKey', |
7 | 10 | 'ISSHKeySet', | 10 | 'ISSHKeySet', |
8 | 11 | 'SSH_KEY_TYPE_TO_TEXT', | ||
9 | 12 | 'SSH_TEXT_TO_KEY_TYPE', | 11 | 'SSH_TEXT_TO_KEY_TYPE', |
10 | 13 | 'SSHKeyAdditionError', | 12 | 'SSHKeyAdditionError', |
11 | 14 | 'SSHKeyType', | 13 | 'SSHKeyType', |
12 | @@ -39,7 +38,7 @@ | |||
13 | 39 | class SSHKeyType(DBEnumeratedType): | 38 | class SSHKeyType(DBEnumeratedType): |
14 | 40 | """SSH key type | 39 | """SSH key type |
15 | 41 | 40 | ||
17 | 42 | SSH (version 2) can use RSA or DSA keys for authentication. See | 41 | SSH (version 2) can use RSA, DSA, or ECDSA keys for authentication. See |
18 | 43 | OpenSSH's ssh-keygen(1) man page for details. | 42 | OpenSSH's ssh-keygen(1) man page for details. |
19 | 44 | """ | 43 | """ |
20 | 45 | 44 | ||
21 | @@ -55,14 +54,20 @@ | |||
22 | 55 | DSA | 54 | DSA |
23 | 56 | """) | 55 | """) |
24 | 57 | 56 | ||
33 | 58 | 57 | ECDSA = DBItem(3, """ | |
34 | 59 | SSH_KEY_TYPE_TO_TEXT = { | 58 | ECDSA |
35 | 60 | SSHKeyType.RSA: "ssh-rsa", | 59 | |
36 | 61 | SSHKeyType.DSA: "ssh-dss", | 60 | ECDSA |
37 | 62 | } | 61 | """) |
38 | 63 | 62 | ||
39 | 64 | 63 | ||
40 | 65 | SSH_TEXT_TO_KEY_TYPE = {v: k for k, v in SSH_KEY_TYPE_TO_TEXT.items()} | 64 | SSH_TEXT_TO_KEY_TYPE = { |
41 | 65 | "ssh-rsa": SSHKeyType.RSA, | ||
42 | 66 | "ssh-dss": SSHKeyType.DSA, | ||
43 | 67 | "ecdsa-sha2-nistp256": SSHKeyType.ECDSA, | ||
44 | 68 | "ecdsa-sha2-nistp384": SSHKeyType.ECDSA, | ||
45 | 69 | "ecdsa-sha2-nistp521": SSHKeyType.ECDSA, | ||
46 | 70 | } | ||
47 | 66 | 71 | ||
48 | 67 | 72 | ||
49 | 68 | class ISSHKey(Interface): | 73 | class ISSHKey(Interface): |
50 | @@ -139,6 +144,11 @@ | |||
51 | 139 | if 'kind' in kwargs: | 144 | if 'kind' in kwargs: |
52 | 140 | kind = kwargs.pop('kind') | 145 | kind = kwargs.pop('kind') |
53 | 141 | msg = "Invalid SSH key type: '%s'" % kind | 146 | msg = "Invalid SSH key type: '%s'" % kind |
54 | 147 | if 'type_mismatch' in kwargs: | ||
55 | 148 | keytype, keydatatype = kwargs.pop('type_mismatch') | ||
56 | 149 | msg = ( | ||
57 | 150 | "Invalid SSH key data: key type '%s' does not match key data " | ||
58 | 151 | "type '%s'" % (keytype, keydatatype)) | ||
59 | 142 | if 'exception' in kwargs: | 152 | if 'exception' in kwargs: |
60 | 143 | exception = kwargs.pop('exception') | 153 | exception = kwargs.pop('exception') |
61 | 144 | try: | 154 | try: |
62 | 145 | 155 | ||
63 | === modified file 'lib/lp/registry/model/person.py' | |||
64 | --- lib/lp/registry/model/person.py 2018-06-19 14:46:05 +0000 | |||
65 | +++ lib/lp/registry/model/person.py 2018-07-02 14:38:50 +0000 | |||
66 | @@ -29,6 +29,7 @@ | |||
67 | 29 | 'WikiNameSet', | 29 | 'WikiNameSet', |
68 | 30 | ] | 30 | ] |
69 | 31 | 31 | ||
70 | 32 | import base64 | ||
71 | 32 | from datetime import ( | 33 | from datetime import ( |
72 | 33 | datetime, | 34 | datetime, |
73 | 34 | timedelta, | 35 | timedelta, |
74 | @@ -81,6 +82,7 @@ | |||
75 | 81 | Store, | 82 | Store, |
76 | 82 | ) | 83 | ) |
77 | 83 | import transaction | 84 | import transaction |
78 | 85 | from twisted.conch.ssh.common import getNS | ||
79 | 84 | from twisted.conch.ssh.keys import Key | 86 | from twisted.conch.ssh.keys import Key |
80 | 85 | from zope.component import ( | 87 | from zope.component import ( |
81 | 86 | adapter, | 88 | adapter, |
82 | @@ -205,7 +207,6 @@ | |||
83 | 205 | from lp.registry.interfaces.ssh import ( | 207 | from lp.registry.interfaces.ssh import ( |
84 | 206 | ISSHKey, | 208 | ISSHKey, |
85 | 207 | ISSHKeySet, | 209 | ISSHKeySet, |
86 | 208 | SSH_KEY_TYPE_TO_TEXT, | ||
87 | 209 | SSH_TEXT_TO_KEY_TYPE, | 210 | SSH_TEXT_TO_KEY_TYPE, |
88 | 210 | SSHKeyAdditionError, | 211 | SSHKeyAdditionError, |
89 | 211 | SSHKeyType, | 212 | SSHKeyType, |
90 | @@ -4122,8 +4123,23 @@ | |||
91 | 4122 | super(SSHKey, self).destroySelf() | 4123 | super(SSHKey, self).destroySelf() |
92 | 4123 | 4124 | ||
93 | 4124 | def getFullKeyText(self): | 4125 | def getFullKeyText(self): |
96 | 4125 | return "%s %s %s" % ( | 4126 | try: |
97 | 4126 | SSH_KEY_TYPE_TO_TEXT[self.keytype], self.keytext, self.comment) | 4127 | ssh_keytype = getNS(base64.b64decode(self.keytext))[0] |
98 | 4128 | except Exception as e: | ||
99 | 4129 | # We didn't always validate keys, so there might be some that | ||
100 | 4130 | # can't be loaded this way. | ||
101 | 4131 | if self.keytype == SSHKeyType.RSA: | ||
102 | 4132 | ssh_keytype = "ssh-rsa" | ||
103 | 4133 | elif self.keytype == SSHKeyType.DSA: | ||
104 | 4134 | ssh_keytype = "ssh-dss" | ||
105 | 4135 | else: | ||
106 | 4136 | # There's no single ssh_keytype for ECDSA keys (it depends | ||
107 | 4137 | # on the curve), and we've always validated these so this | ||
108 | 4138 | # shouldn't happen. | ||
109 | 4139 | raise ValueError( | ||
110 | 4140 | "SSH key of type %s has invalid data '%s'" % | ||
111 | 4141 | (self.keytype, self.keytext)) | ||
112 | 4142 | return "%s %s %s" % (ssh_keytype, self.keytext, self.comment) | ||
113 | 4127 | 4143 | ||
114 | 4128 | 4144 | ||
115 | 4129 | @implementer(ISSHKeySet) | 4145 | @implementer(ISSHKeySet) |
116 | @@ -4131,13 +4147,16 @@ | |||
117 | 4131 | 4147 | ||
118 | 4132 | def new(self, person, sshkey, check_key=True, send_notification=True, | 4148 | def new(self, person, sshkey, check_key=True, send_notification=True, |
119 | 4133 | dry_run=False): | 4149 | dry_run=False): |
121 | 4134 | keytype, keytext, comment = self._extract_ssh_key_components(sshkey) | 4150 | kind, keytype, keytext, comment = self._extract_ssh_key_components( |
122 | 4151 | sshkey) | ||
123 | 4135 | 4152 | ||
124 | 4136 | if check_key: | 4153 | if check_key: |
125 | 4137 | try: | 4154 | try: |
127 | 4138 | Key.fromString(sshkey) | 4155 | key = Key.fromString(sshkey) |
128 | 4139 | except Exception as e: | 4156 | except Exception as e: |
129 | 4140 | raise SSHKeyAdditionError(key=sshkey, exception=e) | 4157 | raise SSHKeyAdditionError(key=sshkey, exception=e) |
130 | 4158 | if kind != key.sshType(): | ||
131 | 4159 | raise SSHKeyAdditionError(type_mismatch=(kind, key.sshType())) | ||
132 | 4141 | 4160 | ||
133 | 4142 | if send_notification: | 4161 | if send_notification: |
134 | 4143 | person.security_field_changed( | 4162 | person.security_field_changed( |
135 | @@ -4161,7 +4180,7 @@ | |||
136 | 4161 | """ % sqlvalues([person.id for person in people])) | 4180 | """ % sqlvalues([person.id for person in people])) |
137 | 4162 | 4181 | ||
138 | 4163 | def getByPersonAndKeyText(self, person, sshkey): | 4182 | def getByPersonAndKeyText(self, person, sshkey): |
140 | 4164 | keytype, keytext, comment = self._extract_ssh_key_components(sshkey) | 4183 | _, keytype, keytext, comment = self._extract_ssh_key_components(sshkey) |
141 | 4165 | return IStore(SSHKey).find( | 4184 | return IStore(SSHKey).find( |
142 | 4166 | SSHKey, | 4185 | SSHKey, |
143 | 4167 | person=person, keytype=keytype, keytext=keytext, comment=comment) | 4186 | person=person, keytype=keytype, keytext=keytext, comment=comment) |
144 | @@ -4178,7 +4197,7 @@ | |||
145 | 4178 | keytype = SSH_TEXT_TO_KEY_TYPE.get(kind) | 4197 | keytype = SSH_TEXT_TO_KEY_TYPE.get(kind) |
146 | 4179 | if keytype is None: | 4198 | if keytype is None: |
147 | 4180 | raise SSHKeyAdditionError(kind=kind) | 4199 | raise SSHKeyAdditionError(kind=kind) |
149 | 4181 | return keytype, keytext, comment | 4200 | return kind, keytype, keytext, comment |
150 | 4182 | 4201 | ||
151 | 4183 | 4202 | ||
152 | 4184 | @implementer(IWikiName) | 4203 | @implementer(IWikiName) |
153 | 4185 | 4204 | ||
154 | === modified file 'lib/lp/registry/scripts/listteammembers.py' | |||
155 | --- lib/lp/registry/scripts/listteammembers.py 2016-05-23 03:17:16 +0000 | |||
156 | +++ lib/lp/registry/scripts/listteammembers.py 2018-07-02 14:38:50 +0000 | |||
157 | @@ -11,7 +11,6 @@ | |||
158 | 11 | from zope.component import getUtility | 11 | from zope.component import getUtility |
159 | 12 | 12 | ||
160 | 13 | from lp.registry.interfaces.person import IPersonSet | 13 | from lp.registry.interfaces.person import IPersonSet |
161 | 14 | from lp.registry.interfaces.ssh import SSH_KEY_TYPE_TO_TEXT | ||
162 | 15 | 14 | ||
163 | 16 | 15 | ||
164 | 17 | OUTPUT_TEMPLATES = { | 16 | OUTPUT_TEMPLATES = { |
165 | @@ -30,11 +29,8 @@ | |||
166 | 30 | bad_ssh_pattern = re.compile('[\r\n\f]') | 29 | bad_ssh_pattern = re.compile('[\r\n\f]') |
167 | 31 | 30 | ||
168 | 32 | 31 | ||
174 | 33 | def make_sshkey_params(member, type_name, key): | 32 | def make_sshkey_params(member, key): |
175 | 34 | sshkey = "%s %s %s" % ( | 33 | sshkey = bad_ssh_pattern.sub('', key.getFullKeyText()).strip() |
171 | 35 | type_name, | ||
172 | 36 | bad_ssh_pattern.sub('', key.keytext), | ||
173 | 37 | bad_ssh_pattern.sub('', key.comment).strip()) | ||
176 | 38 | return dict(name=member.name, sshkey=sshkey) | 34 | return dict(name=member.name, sshkey=sshkey) |
177 | 39 | 35 | ||
178 | 40 | 36 | ||
179 | @@ -62,8 +58,7 @@ | |||
180 | 62 | sshkey = '--none--' | 58 | sshkey = '--none--' |
181 | 63 | if display_option == 'sshkeys': | 59 | if display_option == 'sshkeys': |
182 | 64 | for key in member.sshkeys: | 60 | for key in member.sshkeys: |
185 | 65 | type_name = SSH_KEY_TYPE_TO_TEXT[key.keytype] | 61 | params = make_sshkey_params(member, key) |
184 | 66 | params = make_sshkey_params(member, type_name, key) | ||
186 | 67 | output.append(template % params) | 62 | output.append(template % params) |
187 | 68 | # Ubuntite | 63 | # Ubuntite |
188 | 69 | ubuntite = "no" | 64 | ubuntite = "no" |
189 | 70 | 65 | ||
190 | === modified file 'lib/lp/registry/stories/person/xx-add-sshkey.txt' | |||
191 | --- lib/lp/registry/stories/person/xx-add-sshkey.txt 2017-01-11 18:45:55 +0000 | |||
192 | +++ lib/lp/registry/stories/person/xx-add-sshkey.txt 2018-07-02 14:38:50 +0000 | |||
193 | @@ -57,8 +57,9 @@ | |||
194 | 57 | Change your SSH keys... | 57 | Change your SSH keys... |
195 | 58 | 58 | ||
196 | 59 | Any key must be of the form "keytype keytext comment", where keytype must be | 59 | Any key must be of the form "keytype keytext comment", where keytype must be |
199 | 60 | either ssh-rsa or ssh-dss. If the key doesn't match the expected format, an | 60 | one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp256, ecdsa-sha2-nistp284, or |
200 | 61 | error message will be shown. | 61 | ecdsa-sha2-nistp521. If the key doesn't match the expected format, an error |
201 | 62 | message will be shown. | ||
202 | 62 | 63 | ||
203 | 63 | >>> sshkey = "ssh-rsa " | 64 | >>> sshkey = "ssh-rsa " |
204 | 64 | >>> browser.getControl(name='sshkey').value = sshkey | 65 | >>> browser.getControl(name='sshkey').value = sshkey |
205 | @@ -82,7 +83,7 @@ | |||
206 | 82 | Invalid public key | 83 | Invalid public key |
207 | 83 | 84 | ||
208 | 84 | 85 | ||
210 | 85 | Now, Salgado will upload one RSA and one DSA keys, matching the expected | 86 | Now, Salgado will upload one of each type of key, matching the expected |
211 | 86 | format. | 87 | format. |
212 | 87 | 88 | ||
213 | 88 | >>> sshkey = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6VVQrIoBhxSB7duD69PRzYfdJz3QNUky5lSOpl6a9hEP9iAU72RK3fr4uaYiEEjr70EDAROCimi/rtkBuWCRmPJbQDpzBoZ7PDW/jF5tWAuC4+5z/fy05HOhHRH8WGzeEuWn5HBflcx1QasMD95oDiiEuQbF/kGxBM5/no/4FeJU3fgc+1XQNH7tMDQIcaqoHarc2kefGC8/sbRwbzajhg9yeqskgs6o6y+7931/bcZSLZ/wU53m5nB7eVkkVihk7KD+sf9jKG91LnaRW1IjBgo8AAbXl+e556XkwIwVoieKNYW2Fvw8ybcW5rCTvJ1e/3Cvo2hw8ZsDMRofSifiKw== salgado@canario" | 89 | >>> sshkey = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6VVQrIoBhxSB7duD69PRzYfdJz3QNUky5lSOpl6a9hEP9iAU72RK3fr4uaYiEEjr70EDAROCimi/rtkBuWCRmPJbQDpzBoZ7PDW/jF5tWAuC4+5z/fy05HOhHRH8WGzeEuWn5HBflcx1QasMD95oDiiEuQbF/kGxBM5/no/4FeJU3fgc+1XQNH7tMDQIcaqoHarc2kefGC8/sbRwbzajhg9yeqskgs6o6y+7931/bcZSLZ/wU53m5nB7eVkkVihk7KD+sf9jKG91LnaRW1IjBgo8AAbXl+e556XkwIwVoieKNYW2Fvw8ybcW5rCTvJ1e/3Cvo2hw8ZsDMRofSifiKw== salgado@canario" |
214 | @@ -101,6 +102,30 @@ | |||
215 | 101 | ... print tag.renderContents() | 102 | ... print tag.renderContents() |
216 | 102 | SSH public key added. | 103 | SSH public key added. |
217 | 103 | 104 | ||
218 | 105 | >>> sshkey = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJseCUmxVG7D6qh4JmhLp0Du4kScScJ9PtZ0LGHYHaURnRw9tbX1wwURAio8og6dbnT75CQ3TbUE/xJhxI0aFXE= salgado@canario" | ||
219 | 106 | >>> browser.getControl(name='sshkey').value = sshkey | ||
220 | 107 | >>> browser.getControl('Import Public Key').click() | ||
221 | 108 | >>> soup = find_main_content(browser.contents) | ||
222 | 109 | >>> for tag in soup('p', 'informational message'): | ||
223 | 110 | ... print tag.renderContents() | ||
224 | 111 | SSH public key added. | ||
225 | 112 | |||
226 | 113 | >>> sshkey = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBDUR0E0zCHRHJER6uzjfE/o0HAHFLcq/n8lp0duThpeIPsmo+wr3vHHuAAyOddOgkuQC8Lj8FzHlrOEYgXL6qa7FvpviE9YWUgmqVDa/yJbL/m6Mg8fvSIXlDJKmvOSv6g== salgado@canario" | ||
227 | 114 | >>> browser.getControl(name='sshkey').value = sshkey | ||
228 | 115 | >>> browser.getControl('Import Public Key').click() | ||
229 | 116 | >>> soup = find_main_content(browser.contents) | ||
230 | 117 | >>> for tag in soup('p', 'informational message'): | ||
231 | 118 | ... print tag.renderContents() | ||
232 | 119 | SSH public key added. | ||
233 | 120 | |||
234 | 121 | >>> sshkey = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAB3rpD+Ozb/kwUOqCZUXSiruAkIx6sNZLJyjJ0zxVTZSannaysCLxMQ/IiVxCd59+U2NaLduMzd93JcYDRlX3M5+AApY+3JjfSPo01Sb17HTLNSYU3RZWx0A3XJxm/YN+x/iuYZ3IziuAKeYMsNsdfHlO4/IWjw4Ruy0enW+QhWaY2qAQ== salgado@canario" | ||
235 | 122 | >>> browser.getControl(name='sshkey').value = sshkey | ||
236 | 123 | >>> browser.getControl('Import Public Key').click() | ||
237 | 124 | >>> soup = find_main_content(browser.contents) | ||
238 | 125 | >>> for tag in soup('p', 'informational message'): | ||
239 | 126 | ... print tag.renderContents() | ||
240 | 127 | SSH public key added. | ||
241 | 128 | |||
242 | 104 | Launchpad administrators are not allowed to poke at other user's ssh keys. | 129 | Launchpad administrators are not allowed to poke at other user's ssh keys. |
243 | 105 | 130 | ||
244 | 106 | >>> login(ANONYMOUS) | 131 | >>> login(ANONYMOUS) |
245 | @@ -118,11 +143,13 @@ | |||
246 | 118 | >>> browser.open('http://launchpad.dev/~salgado') | 143 | >>> browser.open('http://launchpad.dev/~salgado') |
247 | 119 | >>> print browser.title | 144 | >>> print browser.title |
248 | 120 | Guilherme Salgado in Launchpad | 145 | Guilherme Salgado in Launchpad |
251 | 121 | >>> print extract_text( | 146 | >>> print extract_text(find_tag_by_id(browser.contents, 'sshkeys')) |
250 | 122 | ... find_tag_by_id(browser.contents, 'sshkeys')) | ||
252 | 123 | SSH keys: Update SSH keys | 147 | SSH keys: Update SSH keys |
253 | 124 | salgado@canario | 148 | salgado@canario |
254 | 125 | salgado@canario | 149 | salgado@canario |
255 | 150 | salgado@canario | ||
256 | 151 | salgado@canario | ||
257 | 152 | salgado@canario | ||
258 | 126 | >>> browser.getLink('Update SSH keys').click() | 153 | >>> browser.getLink('Update SSH keys').click() |
259 | 127 | >>> print browser.title | 154 | >>> print browser.title |
260 | 128 | Change your SSH keys... | 155 | Change your SSH keys... |
261 | 129 | 156 | ||
262 | === modified file 'lib/lp/registry/templates/person-editsshkeys.pt' | |||
263 | --- lib/lp/registry/templates/person-editsshkeys.pt 2012-05-31 02:20:41 +0000 | |||
264 | +++ lib/lp/registry/templates/person-editsshkeys.pt 2018-07-02 14:38:50 +0000 | |||
265 | @@ -47,7 +47,8 @@ | |||
266 | 47 | <label>Public key line</label> | 47 | <label>Public key line</label> |
267 | 48 | <div class="formHelp"> | 48 | <div class="formHelp"> |
268 | 49 | Insert the contents of your public key (usually | 49 | Insert the contents of your public key (usually |
270 | 50 | <code>~/.ssh/id_dsa.pub</code> or <code>~/.ssh/id_rsa.pub</code>). | 50 | <code>~/.ssh/id_rsa.pub</code>, <code>~/.ssh/id_dsa.pub</code>, or |
271 | 51 | <code>~/.ssh/id_ecdsa.pub</code>). | ||
272 | 51 | Only SSH v2 keys are supported. | 52 | Only SSH v2 keys are supported. |
273 | 52 | <a href="https://help.launchpad.net/YourAccount/CreatingAnSSHKeyPair"> | 53 | <a href="https://help.launchpad.net/YourAccount/CreatingAnSSHKeyPair"> |
274 | 53 | How do I create a public key? | 54 | How do I create a public key? |
275 | 54 | 55 | ||
276 | === modified file 'lib/lp/registry/tests/test_ssh.py' | |||
277 | --- lib/lp/registry/tests/test_ssh.py 2018-06-25 15:14:38 +0000 | |||
278 | +++ lib/lp/registry/tests/test_ssh.py 2018-07-02 14:38:50 +0000 | |||
279 | @@ -12,7 +12,6 @@ | |||
280 | 12 | ISSHKeySet, | 12 | ISSHKeySet, |
281 | 13 | SSH_TEXT_TO_KEY_TYPE, | 13 | SSH_TEXT_TO_KEY_TYPE, |
282 | 14 | SSHKeyAdditionError, | 14 | SSHKeyAdditionError, |
283 | 15 | SSHKeyType, | ||
284 | 16 | ) | 15 | ) |
285 | 17 | from lp.testing import ( | 16 | from lp.testing import ( |
286 | 18 | admin_logged_in, | 17 | admin_logged_in, |
287 | @@ -31,17 +30,38 @@ | |||
288 | 31 | def test_getFullKeyText_for_rsa_key(self): | 30 | def test_getFullKeyText_for_rsa_key(self): |
289 | 32 | person = self.factory.makePerson() | 31 | person = self.factory.makePerson() |
290 | 33 | with person_logged_in(person): | 32 | with person_logged_in(person): |
292 | 34 | key = self.factory.makeSSHKey(person, SSHKeyType.RSA) | 33 | key = self.factory.makeSSHKey(person, "ssh-rsa") |
293 | 35 | expected = "ssh-rsa %s %s" % (key.keytext, key.comment) | 34 | expected = "ssh-rsa %s %s" % (key.keytext, key.comment) |
294 | 36 | self.assertEqual(expected, key.getFullKeyText()) | 35 | self.assertEqual(expected, key.getFullKeyText()) |
295 | 37 | 36 | ||
296 | 38 | def test_getFullKeyText_for_dsa_key(self): | 37 | def test_getFullKeyText_for_dsa_key(self): |
297 | 39 | person = self.factory.makePerson() | 38 | person = self.factory.makePerson() |
298 | 40 | with person_logged_in(person): | 39 | with person_logged_in(person): |
300 | 41 | key = self.factory.makeSSHKey(person, SSHKeyType.DSA) | 40 | key = self.factory.makeSSHKey(person, "ssh-dss") |
301 | 42 | expected = "ssh-dss %s %s" % (key.keytext, key.comment) | 41 | expected = "ssh-dss %s %s" % (key.keytext, key.comment) |
302 | 43 | self.assertEqual(expected, key.getFullKeyText()) | 42 | self.assertEqual(expected, key.getFullKeyText()) |
303 | 44 | 43 | ||
304 | 44 | def test_getFullKeyText_for_ecdsa_nistp256_key(self): | ||
305 | 45 | person = self.factory.makePerson() | ||
306 | 46 | with person_logged_in(person): | ||
307 | 47 | key = self.factory.makeSSHKey(person, "ecdsa-sha2-nistp256") | ||
308 | 48 | expected = "ecdsa-sha2-nistp256 %s %s" % (key.keytext, key.comment) | ||
309 | 49 | self.assertEqual(expected, key.getFullKeyText()) | ||
310 | 50 | |||
311 | 51 | def test_getFullKeyText_for_ecdsa_nistp384_key(self): | ||
312 | 52 | person = self.factory.makePerson() | ||
313 | 53 | with person_logged_in(person): | ||
314 | 54 | key = self.factory.makeSSHKey(person, "ecdsa-sha2-nistp384") | ||
315 | 55 | expected = "ecdsa-sha2-nistp384 %s %s" % (key.keytext, key.comment) | ||
316 | 56 | self.assertEqual(expected, key.getFullKeyText()) | ||
317 | 57 | |||
318 | 58 | def test_getFullKeyText_for_ecdsa_nistp521_key(self): | ||
319 | 59 | person = self.factory.makePerson() | ||
320 | 60 | with person_logged_in(person): | ||
321 | 61 | key = self.factory.makeSSHKey(person, "ecdsa-sha2-nistp521") | ||
322 | 62 | expected = "ecdsa-sha2-nistp521 %s %s" % (key.keytext, key.comment) | ||
323 | 63 | self.assertEqual(expected, key.getFullKeyText()) | ||
324 | 64 | |||
325 | 45 | def test_destroySelf_sends_notification_by_default(self): | 65 | def test_destroySelf_sends_notification_by_default(self): |
326 | 46 | person = self.factory.makePerson() | 66 | person = self.factory.makePerson() |
327 | 47 | with person_logged_in(person): | 67 | with person_logged_in(person): |
328 | @@ -58,7 +78,7 @@ | |||
329 | 58 | % key.comment) | 78 | % key.comment) |
330 | 59 | ) | 79 | ) |
331 | 60 | 80 | ||
333 | 61 | def test_destroySelf_notications_can_be_supressed(self): | 81 | def test_destroySelf_notifications_can_be_suppressed(self): |
334 | 62 | person = self.factory.makePerson() | 82 | person = self.factory.makePerson() |
335 | 63 | with person_logged_in(person): | 83 | with person_logged_in(person): |
336 | 64 | key = self.factory.makeSSHKey(person, send_notification=False) | 84 | key = self.factory.makeSSHKey(person, send_notification=False) |
337 | @@ -179,6 +199,14 @@ | |||
338 | 179 | ) | 199 | ) |
339 | 180 | self.assertRaisesWithContent( | 200 | self.assertRaisesWithContent( |
340 | 181 | SSHKeyAdditionError, | 201 | SSHKeyAdditionError, |
341 | 202 | "Invalid SSH key data: key type 'ssh-rsa' does not match key " | ||
342 | 203 | "data type 'ssh-dss'", | ||
343 | 204 | keyset.new, | ||
344 | 205 | person, | ||
345 | 206 | 'ssh-rsa ' + self.factory.makeSSHKeyText(key_type='ssh-dss')[8:] | ||
346 | 207 | ) | ||
347 | 208 | self.assertRaisesWithContent( | ||
348 | 209 | SSHKeyAdditionError, | ||
349 | 182 | "Invalid SSH key data: 'None'", | 210 | "Invalid SSH key data: 'None'", |
350 | 183 | keyset.new, | 211 | keyset.new, |
351 | 184 | person, None | 212 | person, None |
352 | 185 | 213 | ||
353 | === modified file 'lib/lp/testing/factory.py' | |||
354 | --- lib/lp/testing/factory.py 2018-06-30 16:09:44 +0000 | |||
355 | +++ lib/lp/testing/factory.py 2018-07-02 14:38:50 +0000 | |||
356 | @@ -20,6 +20,7 @@ | |||
357 | 20 | ] | 20 | ] |
358 | 21 | 21 | ||
359 | 22 | import base64 | 22 | import base64 |
360 | 23 | from cryptography.utils import int_to_bytes | ||
361 | 23 | from datetime import ( | 24 | from datetime import ( |
362 | 24 | datetime, | 25 | datetime, |
363 | 25 | timedelta, | 26 | timedelta, |
364 | @@ -228,11 +229,7 @@ | |||
365 | 228 | SourcePackageUrgency, | 229 | SourcePackageUrgency, |
366 | 229 | ) | 230 | ) |
367 | 230 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet | 231 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet |
373 | 231 | from lp.registry.interfaces.ssh import ( | 232 | from lp.registry.interfaces.ssh import ISSHKeySet |
369 | 232 | ISSHKeySet, | ||
370 | 233 | SSH_KEY_TYPE_TO_TEXT, | ||
371 | 234 | SSHKeyType, | ||
372 | 235 | ) | ||
374 | 236 | from lp.registry.model.commercialsubscription import CommercialSubscription | 233 | from lp.registry.model.commercialsubscription import CommercialSubscription |
375 | 237 | from lp.registry.model.karma import KarmaTotalCache | 234 | from lp.registry.model.karma import KarmaTotalCache |
376 | 238 | from lp.registry.model.milestone import Milestone | 235 | from lp.registry.model.milestone import Milestone |
377 | @@ -4311,37 +4308,56 @@ | |||
378 | 4311 | return getUtility(IHWSubmissionDeviceSet).create( | 4308 | return getUtility(IHWSubmissionDeviceSet).create( |
379 | 4312 | device_driver_link, submission, parent, hal_device_id) | 4309 | device_driver_link, submission, parent, hal_device_id) |
380 | 4313 | 4310 | ||
382 | 4314 | def makeSSHKeyText(self, key_type=SSHKeyType.RSA, comment=None): | 4311 | def makeSSHKeyText(self, key_type="ssh-rsa", comment=None): |
383 | 4315 | """Create new SSH public key text. | 4312 | """Create new SSH public key text. |
384 | 4316 | 4313 | ||
387 | 4317 | :param key_type: If specified, the type of SSH key to generate. Must be | 4314 | :param key_type: If specified, the type of SSH key to generate, as a |
388 | 4318 | a member of SSHKeyType. If unspecified, SSHKeyType.RSA is used. | 4315 | public key algorithm name |
389 | 4316 | (https://www.iana.org/assignments/ssh-parameters/). Must be a | ||
390 | 4317 | member of SSH_TEXT_TO_KEY_TYPE. If unspecified, "ssh-rsa" is | ||
391 | 4318 | used. | ||
392 | 4319 | """ | 4319 | """ |
398 | 4320 | key_type_string = SSH_KEY_TYPE_TO_TEXT.get(key_type) | 4320 | parameters = None |
399 | 4321 | if key_type is None: | 4321 | if key_type == "ssh-rsa": |
395 | 4322 | raise AssertionError( | ||
396 | 4323 | "key_type must be a member of SSHKeyType, not %r" % key_type) | ||
397 | 4324 | if key_type == SSHKeyType.RSA: | ||
400 | 4325 | parameters = [MP(keydata.RSAData[param]) for param in ("e", "n")] | 4322 | parameters = [MP(keydata.RSAData[param]) for param in ("e", "n")] |
402 | 4326 | elif key_type == SSHKeyType.DSA: | 4323 | elif key_type == "ssh-dss": |
403 | 4327 | parameters = [ | 4324 | parameters = [ |
404 | 4328 | MP(keydata.DSAData[param]) for param in ("p", "q", "g", "y")] | 4325 | MP(keydata.DSAData[param]) for param in ("p", "q", "g", "y")] |
406 | 4329 | else: | 4326 | elif key_type.startswith("ecdsa-sha2-"): |
407 | 4327 | curve = key_type[len("ecdsa-sha2-"):] | ||
408 | 4328 | key_size, curve_data = { | ||
409 | 4329 | "nistp256": (256, keydata.ECDatanistp256), | ||
410 | 4330 | "nistp384": (384, keydata.ECDatanistp384), | ||
411 | 4331 | "nistp521": (521, keydata.ECDatanistp521), | ||
412 | 4332 | }.get(curve, (None, None)) | ||
413 | 4333 | if curve_data is not None: | ||
414 | 4334 | key_byte_length = (key_size + 7) // 8 | ||
415 | 4335 | parameters = [ | ||
416 | 4336 | NS(curve_data["curve"][-8:]), | ||
417 | 4337 | NS(b"\x04" + | ||
418 | 4338 | int_to_bytes(curve_data["x"], key_byte_length) + | ||
419 | 4339 | int_to_bytes(curve_data["y"], key_byte_length)), | ||
420 | 4340 | ] | ||
421 | 4341 | if parameters is None: | ||
422 | 4330 | raise AssertionError( | 4342 | raise AssertionError( |
425 | 4331 | "key_type must be a member of SSHKeyType, not %r" % key_type) | 4343 | "key_type must be a member of SSH_TEXT_TO_KEY_TYPE, not %r" % |
426 | 4332 | key_text = base64.b64encode(NS(key_type_string) + b"".join(parameters)) | 4344 | key_type) |
427 | 4345 | key_text = base64.b64encode(NS(key_type) + b"".join(parameters)) | ||
428 | 4333 | if comment is None: | 4346 | if comment is None: |
429 | 4334 | comment = self.getUniqueString() | 4347 | comment = self.getUniqueString() |
431 | 4335 | return "%s %s %s" % (key_type_string, key_text, comment) | 4348 | return "%s %s %s" % (key_type, key_text, comment) |
432 | 4336 | 4349 | ||
434 | 4337 | def makeSSHKey(self, person=None, key_type=SSHKeyType.RSA, | 4350 | def makeSSHKey(self, person=None, key_type="ssh-rsa", |
435 | 4338 | send_notification=True): | 4351 | send_notification=True): |
436 | 4339 | """Create a new SSHKey. | 4352 | """Create a new SSHKey. |
437 | 4340 | 4353 | ||
438 | 4341 | :param person: If specified, the person to attach the key to. If | 4354 | :param person: If specified, the person to attach the key to. If |
439 | 4342 | unspecified, a person is created. | 4355 | unspecified, a person is created. |
442 | 4343 | :param key_type: If specified, the type of SSH key to generate. Must be | 4356 | :param key_type: If specified, the type of SSH key to generate, as a |
443 | 4344 | a member of SSHKeyType. If unspecified, SSHKeyType.RSA is used. | 4357 | public key algorithm name |
444 | 4358 | (https://www.iana.org/assignments/ssh-parameters/). Must be a | ||
445 | 4359 | member of SSH_TEXT_TO_KEY_TYPE. If unspecified, "ssh-rsa" is | ||
446 | 4360 | used. | ||
447 | 4345 | """ | 4361 | """ |
448 | 4346 | if person is None: | 4362 | if person is None: |
449 | 4347 | person = self.makePerson() | 4363 | person = self.makePerson() |