OLD | NEW |
| (Empty) |
1 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
2 # See LICENSE for details. | |
3 | |
4 # | |
5 | |
6 """ | |
7 Implementation of the ssh-userauth service. | |
8 Currently implemented authentication types are public-key and password. | |
9 | |
10 Maintainer: U{Paul Swartz<mailto:z3p@twistedmatrix.com>} | |
11 """ | |
12 | |
13 import struct | |
14 from twisted.conch import error, interfaces | |
15 from twisted.cred import credentials | |
16 from twisted.internet import defer, reactor | |
17 from twisted.python import failure, log | |
18 from common import NS, getNS, MP | |
19 import keys, transport, service | |
20 | |
21 class SSHUserAuthServer(service.SSHService): | |
22 name = 'ssh-userauth' | |
23 loginTimeout = 10 * 60 * 60 # 10 minutes before we disconnect them | |
24 attemptsBeforeDisconnect = 20 # number of attempts to allow before a disconn
ect | |
25 passwordDelay = 1 # number of seconds to delay on a failed password | |
26 protocolMessages = None # set later | |
27 interfaceToMethod = { | |
28 credentials.ISSHPrivateKey : 'publickey', | |
29 credentials.IUsernamePassword : 'password', | |
30 credentials.IPluggableAuthenticationModules : 'keyboard-interactive', | |
31 } | |
32 | |
33 def serviceStarted(self): | |
34 self.authenticatedWith = [] | |
35 self.loginAttempts = 0 | |
36 self.user = None | |
37 self.nextService = None | |
38 self.portal = self.transport.factory.portal | |
39 | |
40 self.supportedAuthentications = [] | |
41 for i in self.portal.listCredentialsInterfaces(): | |
42 if i in self.interfaceToMethod: | |
43 self.supportedAuthentications.append(self.interfaceToMethod[i]) | |
44 | |
45 if not self.transport.isEncrypted('out'): | |
46 if 'password' in self.supportedAuthentications: | |
47 self.supportedAuthentications.remove('password') | |
48 if 'keyboard-interactive' in self.supportedAuthentications: | |
49 self.supportedAuthentications.remove('keyboard-interactive') | |
50 # don't let us transport password in plaintext | |
51 self.cancelLoginTimeout = reactor.callLater(self.loginTimeout, | |
52 self.timeoutAuthentication) | |
53 | |
54 def serviceStopped(self): | |
55 if self.cancelLoginTimeout: | |
56 self.cancelLoginTimeout.cancel() | |
57 self.cancelLoginTimeout = None | |
58 | |
59 def timeoutAuthentication(self): | |
60 self.cancelLoginTimeout = None | |
61 self.transport.sendDisconnect( | |
62 transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, | |
63 'you took too long') | |
64 | |
65 | |
66 def tryAuth(self, kind, user, data): | |
67 log.msg('%s trying auth %s' % (user, kind)) | |
68 if kind not in self.supportedAuthentications: | |
69 return defer.fail(error.ConchError('unsupported authentication, fail
ing')) | |
70 kind = kind.replace('-', '_') | |
71 f = getattr(self,'auth_%s'%kind, None) | |
72 if f: | |
73 ret = f(data) | |
74 if not ret: | |
75 return defer.fail(error.ConchError('%s return None instead of a
Deferred' % kind)) | |
76 else: | |
77 return ret | |
78 return defer.fail(error.ConchError('bad auth type: %s' % kind)) | |
79 | |
80 def ssh_USERAUTH_REQUEST(self, packet): | |
81 user, nextService, method, rest = getNS(packet, 3) | |
82 if user != self.user or nextService != self.nextService: | |
83 self.authenticatedWith = [] # clear auth state | |
84 self.user = user | |
85 self.nextService = nextService | |
86 self.method = method | |
87 d = self.tryAuth(method, user, rest) | |
88 if not d: | |
89 self._ebBadAuth( | |
90 failure.Failure(error.ConchError('auth returned none'))) | |
91 return | |
92 d.addCallbacks(self._cbFinishedAuth) | |
93 d.addErrback(self._ebMaybeBadAuth) | |
94 d.addErrback(self._ebBadAuth) | |
95 return d | |
96 | |
97 def _cbFinishedAuth(self, (interface, avatar, logout)): | |
98 self.transport.avatar = avatar | |
99 self.transport.logoutFunction = logout | |
100 service = self.transport.factory.getService(self.transport, | |
101 self.nextService) | |
102 if not service: | |
103 raise error.ConchError('could not get next service: %s' | |
104 % self.nextService) | |
105 log.msg('%s authenticated with %s' % (self.user, self.method)) | |
106 if self.cancelLoginTimeout: | |
107 self.cancelLoginTimeout.cancel() | |
108 self.cancelLoginTimeout = None | |
109 self.transport.sendPacket(MSG_USERAUTH_SUCCESS, '') | |
110 self.transport.setService(service()) | |
111 | |
112 def _ebMaybeBadAuth(self, reason): | |
113 reason.trap(error.NotEnoughAuthentication) | |
114 self.transport.sendPacket(MSG_USERAUTH_FAILURE, NS(','.join(self.support
edAuthentications))+'\xff') | |
115 | |
116 def _ebBadAuth(self, reason): | |
117 if reason.type == error.IgnoreAuthentication: | |
118 return | |
119 if self.method != 'none': | |
120 log.msg('%s failed auth %s' % (self.user, self.method)) | |
121 log.msg('reason:') | |
122 if reason.type == error.ConchError: | |
123 log.msg(str(reason)) | |
124 else: | |
125 log.msg(reason.getTraceback()) | |
126 self.loginAttempts += 1 | |
127 if self.loginAttempts > self.attemptsBeforeDisconnect: | |
128 self.transport.sendDisconnect(transport.DISCONNECT_NO_MORE_AUTH_
METHODS_AVAILABLE, | |
129 'too many bad auths') | |
130 self.transport.sendPacket(MSG_USERAUTH_FAILURE, NS(','.join(self.support
edAuthentications))+'\x00') | |
131 | |
132 def auth_publickey(self, packet): | |
133 hasSig = ord(packet[0]) | |
134 algName, blob, rest = getNS(packet[1:], 2) | |
135 pubKey = keys.getPublicKeyObject(data = blob) | |
136 signature = hasSig and getNS(rest)[0] or None | |
137 if hasSig: | |
138 b = NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + \ | |
139 NS(self.user) + NS(self.nextService) + NS('publickey') + \ | |
140 chr(hasSig) + NS(keys.objectType(pubKey)) + NS(blob) | |
141 c = credentials.SSHPrivateKey(self.user, algName, blob, b, signature
) | |
142 return self.portal.login(c, None, interfaces.IConchUser) | |
143 else: | |
144 c = credentials.SSHPrivateKey(self.user, algName, blob, None, None) | |
145 return self.portal.login(c, None, interfaces.IConchUser).addErrback( | |
146 self._ebCheckKey, | |
147 packet[1:]) | |
148 | |
149 def _ebCheckKey(self, reason, packet): | |
150 reason.trap(error.ValidPublicKey) | |
151 # if we make it here, it means that the publickey is valid | |
152 self.transport.sendPacket(MSG_USERAUTH_PK_OK, packet) | |
153 return failure.Failure(error.IgnoreAuthentication()) | |
154 | |
155 def auth_password(self, packet): | |
156 password = getNS(packet[1:])[0] | |
157 c = credentials.UsernamePassword(self.user, password) | |
158 return self.portal.login(c, None, interfaces.IConchUser).addErrback( | |
159 self._ebPassword) | |
160 | |
161 def _ebPassword(self, f): | |
162 d = defer.Deferred() | |
163 reactor.callLater(self.passwordDelay, lambda d,f:d.callback(f), d, f) | |
164 return d | |
165 | |
166 def auth_keyboard_interactive(self, packet): | |
167 if hasattr(self, '_pamDeferred'): | |
168 self.transport.sendDisconnect(transport.DISCONNECT_PROTOCOL_ERROR, "
only one keyboard interactive attempt at a time") | |
169 return failure.Failure(error.IgnoreAuthentication()) | |
170 c = credentials.PluggableAuthenticationModules(self.user, self._pamConv) | |
171 return self.portal.login(c, None, interfaces.IConchUser) | |
172 | |
173 def _pamConv(self, items): | |
174 resp = [] | |
175 for message, kind in items: | |
176 if kind == 1: # password | |
177 resp.append((message, 0)) | |
178 elif kind == 2: # text | |
179 resp.append((message, 1)) | |
180 elif kind in (3, 4): | |
181 return defer.fail(error.ConchError('cannot handle PAM 3 or 4 mes
sages')) | |
182 else: | |
183 return defer.fail(error.ConchError('bad PAM auth kind %i' % kind
)) | |
184 packet = NS('')+NS('')+NS('') | |
185 packet += struct.pack('>L', len(resp)) | |
186 for prompt, echo in resp: | |
187 packet += NS(prompt) | |
188 packet += chr(echo) | |
189 self.transport.sendPacket(MSG_USERAUTH_INFO_REQUEST, packet) | |
190 self._pamDeferred = defer.Deferred() | |
191 return self._pamDeferred | |
192 | |
193 def ssh_USERAUTH_INFO_RESPONSE(self, packet): | |
194 d = self._pamDeferred | |
195 del self._pamDeferred | |
196 try: | |
197 resp = [] | |
198 numResps = struct.unpack('>L', packet[:4])[0] | |
199 packet = packet[4:] | |
200 while packet: | |
201 response, packet = getNS(packet) | |
202 resp.append((response, 0)) | |
203 assert len(resp) == numResps | |
204 except: | |
205 d.errback(failure.Failure()) | |
206 else: | |
207 d.callback(resp) | |
208 | |
209 class SSHUserAuthClient(service.SSHService): | |
210 name = 'ssh-userauth' | |
211 protocolMessages = None # set later | |
212 | |
213 preferredOrder = ['publickey', 'password', 'keyboard-interactive'] | |
214 | |
215 def __init__(self, user, instance): | |
216 self.user = user | |
217 self.instance = instance | |
218 | |
219 def serviceStarted(self): | |
220 self.authenticatedWith = [] | |
221 self.triedPublicKeys = [] | |
222 self.lastPublicKey = None | |
223 self.askForAuth('none', '') | |
224 | |
225 def askForAuth(self, kind, extraData): | |
226 self.lastAuth = kind | |
227 self.transport.sendPacket(MSG_USERAUTH_REQUEST, NS(self.user) + \ | |
228 NS(self.instance.name) + NS(kind) + extraData) | |
229 def tryAuth(self, kind): | |
230 kind = kind.replace('-', '_') | |
231 log.msg('trying to auth with %s' % kind) | |
232 f= getattr(self,'auth_%s'%kind, None) | |
233 if f: | |
234 return f() | |
235 | |
236 def _ebAuth(self, ignored, *args): | |
237 self.tryAuth('none') | |
238 | |
239 def ssh_USERAUTH_SUCCESS(self, packet): | |
240 self.transport.setService(self.instance) | |
241 #self.ssh_USERAUTH_SUCCESS = lambda *a: None # ignore these | |
242 | |
243 def ssh_USERAUTH_FAILURE(self, packet): | |
244 canContinue, partial = getNS(packet) | |
245 canContinue = canContinue.split(',') | |
246 partial = ord(partial) | |
247 if partial: | |
248 self.authenticatedWith.append(self.lastAuth) | |
249 def _(x, y): | |
250 try: | |
251 i1 = self.preferredOrder.index(x) | |
252 except ValueError: | |
253 return 1 | |
254 try: | |
255 i2 = self.preferredOrder.index(y) | |
256 except ValueError: | |
257 return -1 | |
258 return cmp(i1, i2) | |
259 canContinue.sort(_) | |
260 log.msg('can continue with: %s' % canContinue) | |
261 for method in canContinue: | |
262 if method not in self.authenticatedWith and self.tryAuth(method): | |
263 return | |
264 self.transport.sendDisconnect(transport.DISCONNECT_NO_MORE_AUTH_METHODS_
AVAILABLE, 'no more authentication methods available') | |
265 | |
266 def ssh_USERAUTH_PK_OK(self, packet): | |
267 if self.lastAuth == 'publickey': | |
268 # this is ok | |
269 publicKey = self.lastPublicKey | |
270 keyType = getNS(publicKey)[0] | |
271 b = NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + \ | |
272 NS(self.user) + NS(self.instance.name) + NS('publickey') + '\xff' +\ | |
273 NS(keyType) + NS(publicKey) | |
274 d = self.signData(publicKey, b) | |
275 if not d: | |
276 self.askForAuth('none', '') | |
277 # this will fail, we'll move on | |
278 return | |
279 d.addCallback(self._cbSignedData) | |
280 d.addErrback(self._ebAuth) | |
281 elif self.lastAuth == 'password': | |
282 prompt, language, rest = getNS(packet, 2) | |
283 self._oldPass = self._newPass = None | |
284 self.getPassword('Old Password: ').addCallbacks(self._setOldPass, se
lf._ebAuth) | |
285 self.getPassword(prompt).addCallbacks(self._setNewPass, self._ebAuth
) | |
286 elif self.lastAuth == 'keyboard-interactive': | |
287 name, instruction, lang, data = getNS(packet, 3) | |
288 numPrompts = struct.unpack('!L', data[:4])[0] | |
289 data = data[4:] | |
290 prompts = [] | |
291 for i in range(numPrompts): | |
292 prompt, data = getNS(data) | |
293 echo = bool(ord(data[0])) | |
294 data = data[1:] | |
295 prompts.append((prompt, echo)) | |
296 d = self.getGenericAnswers(name, instruction, prompts) | |
297 d.addCallback(self._cbGenericAnswers) | |
298 d.addErrback(self._ebAuth) | |
299 | |
300 def _cbSignedData(self, signedData): | |
301 publicKey = self.lastPublicKey | |
302 keyType = getNS(publicKey)[0] | |
303 self.askForAuth('publickey', '\xff' + NS(keyType) + NS(publicKey) + \ | |
304 NS(signedData)) | |
305 | |
306 | |
307 | |
308 | |
309 | |
310 | |
311 def _setOldPass(self, op): | |
312 if self._newPass: | |
313 np = self._newPass | |
314 self._newPass = None | |
315 self.askForAuth('password', '\xff'+NS(op)+NS(np)) | |
316 else: | |
317 self._oldPass = op | |
318 | |
319 def _setNewPass(self, np): | |
320 if self._oldPass: | |
321 op = self._oldPass | |
322 self._oldPass = None | |
323 self.askForAuth('password', '\xff'+NS(op)+NS(np)) | |
324 else: | |
325 self._newPass = np | |
326 | |
327 def _cbGenericAnswers(self, responses): | |
328 data = struct.pack('!L', len(responses)) | |
329 for r in responses: | |
330 data += NS(r.encode('UTF8')) | |
331 self.transport.sendPacket(MSG_USERAUTH_INFO_RESPONSE, data) | |
332 | |
333 def auth_publickey(self): | |
334 publicKey = self.getPublicKey() | |
335 if publicKey: | |
336 self.lastPublicKey = publicKey | |
337 self.triedPublicKeys.append(publicKey) | |
338 keyType = getNS(publicKey)[0] | |
339 log.msg('using key of type %s' % keyType) | |
340 self.askForAuth('publickey', '\x00' + NS(keyType) + \ | |
341 NS(publicKey)) | |
342 return 1 | |
343 else: | |
344 return 0 | |
345 | |
346 def auth_password(self): | |
347 d = self.getPassword() | |
348 if d: | |
349 d.addCallbacks(self._cbPassword, self._ebAuth) | |
350 return 1 | |
351 else: # returned None, don't do password auth | |
352 return 0 | |
353 | |
354 def auth_keyboard_interactive(self): | |
355 log.msg('authing with keyboard-interactive') | |
356 self.askForAuth('keyboard-interactive', NS('') + NS('')) | |
357 return 1 | |
358 | |
359 def _cbPassword(self, password): | |
360 self.askForAuth('password', '\x00'+NS(password)) | |
361 | |
362 def signData(self, publicKey, signData): | |
363 """ | |
364 Sign the given data with the given public key blob. | |
365 By default, this will call getPrivateKey to get the private key, | |
366 the sign the data using keys.signData. | |
367 However, this is factored out so that it can use alternate methods, | |
368 such as a key agent. | |
369 """ | |
370 key = self.getPrivateKey() | |
371 if not key: | |
372 return | |
373 return key.addCallback(self._cbSignData, signData) | |
374 | |
375 def _cbSignData(self, privateKey, signData): | |
376 return keys.signData(privateKey, signData) | |
377 | |
378 def getPublicKey(self): | |
379 """ | |
380 Return a public key for the user. If no more public keys are | |
381 available, return None. | |
382 | |
383 @rtype: C{str}/C{None} | |
384 """ | |
385 return None | |
386 #raise NotImplementedError | |
387 | |
388 | |
389 def getPrivateKey(self): | |
390 """ | |
391 Return a L{Deferred} that will be called back with the private key | |
392 corresponding to the last public key from getPublicKey(). | |
393 If the private key is not available, errback on the Deferred. | |
394 | |
395 @rtype: L{Deferred} | |
396 """ | |
397 return defer.fail(NotImplementedError()) | |
398 | |
399 def getPassword(self, prompt = None): | |
400 """ | |
401 Return a L{Deferred} that will be called back with a password. | |
402 prompt is a string to display for the password, or None for a generic | |
403 'user@hostname's password: '. | |
404 | |
405 @type prompt: C{str}/C{None} | |
406 @rtype: L{Deferred} | |
407 """ | |
408 return defer.fail(NotImplementedError()) | |
409 | |
410 def getGenericAnswers(self, name, instruction, prompts): | |
411 """ | |
412 Returns a L{Deferred} with the responses to the promopts. | |
413 | |
414 @param name: The name of the authentication currently in progress. | |
415 @param instruction: Describes what the authentication wants. | |
416 @param prompts: A list of (prompt, echo) pairs, where prompt is a | |
417 string to display and echo is a boolean indicating whether the | |
418 user's response should be echoed as they type it. | |
419 """ | |
420 return defer.fail(NotImplementedError()) | |
421 | |
422 MSG_USERAUTH_REQUEST = 50 | |
423 MSG_USERAUTH_FAILURE = 51 | |
424 MSG_USERAUTH_SUCCESS = 52 | |
425 MSG_USERAUTH_BANNER = 53 | |
426 MSG_USERAUTH_PASSWD_CHANGEREQ = 60 | |
427 MSG_USERAUTH_INFO_REQUEST = 60 | |
428 MSG_USERAUTH_INFO_RESPONSE = 61 | |
429 MSG_USERAUTH_PK_OK = 60 | |
430 | |
431 messages = {} | |
432 import userauth | |
433 for v in dir(userauth): | |
434 if v[:4]=='MSG_': | |
435 messages[getattr(userauth,v)] = v # doesn't handle doubles | |
436 | |
437 SSHUserAuthServer.protocolMessages = messages | |
438 SSHUserAuthClient.protocolMessages = messages | |
OLD | NEW |