Coverage for /pythoncovmergedfiles/medio/medio/src/paramiko/paramiko/auth_handler.py: 12%
564 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:36 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:36 +0000
1# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
2#
3# This file is part of paramiko.
4#
5# Paramiko is free software; you can redistribute it and/or modify it under the
6# terms of the GNU Lesser General Public License as published by the Free
7# Software Foundation; either version 2.1 of the License, or (at your option)
8# any later version.
9#
10# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
13# details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19"""
20`.AuthHandler`
21"""
23import weakref
24import time
25import re
27from paramiko.common import (
28 cMSG_SERVICE_REQUEST,
29 cMSG_DISCONNECT,
30 DISCONNECT_SERVICE_NOT_AVAILABLE,
31 DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
32 cMSG_USERAUTH_REQUEST,
33 cMSG_SERVICE_ACCEPT,
34 DEBUG,
35 AUTH_SUCCESSFUL,
36 INFO,
37 cMSG_USERAUTH_SUCCESS,
38 cMSG_USERAUTH_FAILURE,
39 AUTH_PARTIALLY_SUCCESSFUL,
40 cMSG_USERAUTH_INFO_REQUEST,
41 WARNING,
42 AUTH_FAILED,
43 cMSG_USERAUTH_PK_OK,
44 cMSG_USERAUTH_INFO_RESPONSE,
45 MSG_SERVICE_REQUEST,
46 MSG_SERVICE_ACCEPT,
47 MSG_USERAUTH_REQUEST,
48 MSG_USERAUTH_SUCCESS,
49 MSG_USERAUTH_FAILURE,
50 MSG_USERAUTH_BANNER,
51 MSG_USERAUTH_INFO_REQUEST,
52 MSG_USERAUTH_INFO_RESPONSE,
53 cMSG_USERAUTH_GSSAPI_RESPONSE,
54 cMSG_USERAUTH_GSSAPI_TOKEN,
55 cMSG_USERAUTH_GSSAPI_MIC,
56 MSG_USERAUTH_GSSAPI_RESPONSE,
57 MSG_USERAUTH_GSSAPI_TOKEN,
58 MSG_USERAUTH_GSSAPI_ERROR,
59 MSG_USERAUTH_GSSAPI_ERRTOK,
60 MSG_USERAUTH_GSSAPI_MIC,
61 MSG_NAMES,
62 cMSG_USERAUTH_BANNER,
63)
64from paramiko.message import Message
65from paramiko.util import b, u
66from paramiko.ssh_exception import (
67 SSHException,
68 AuthenticationException,
69 BadAuthenticationType,
70 PartialAuthentication,
71)
72from paramiko.server import InteractiveQuery
73from paramiko.ssh_gss import GSSAuth, GSS_EXCEPTIONS
76class AuthHandler:
77 """
78 Internal class to handle the mechanics of authentication.
79 """
81 def __init__(self, transport):
82 self.transport = weakref.proxy(transport)
83 self.username = None
84 self.authenticated = False
85 self.auth_event = None
86 self.auth_method = ""
87 self.banner = None
88 self.password = None
89 self.private_key = None
90 self.interactive_handler = None
91 self.submethods = None
92 # for server mode:
93 self.auth_username = None
94 self.auth_fail_count = 0
95 # for GSSAPI
96 self.gss_host = None
97 self.gss_deleg_creds = True
99 def _log(self, *args):
100 return self.transport._log(*args)
102 def is_authenticated(self):
103 return self.authenticated
105 def get_username(self):
106 if self.transport.server_mode:
107 return self.auth_username
108 else:
109 return self.username
111 def auth_none(self, username, event):
112 self.transport.lock.acquire()
113 try:
114 self.auth_event = event
115 self.auth_method = "none"
116 self.username = username
117 self._request_auth()
118 finally:
119 self.transport.lock.release()
121 def auth_publickey(self, username, key, event):
122 self.transport.lock.acquire()
123 try:
124 self.auth_event = event
125 self.auth_method = "publickey"
126 self.username = username
127 self.private_key = key
128 self._request_auth()
129 finally:
130 self.transport.lock.release()
132 def auth_password(self, username, password, event):
133 self.transport.lock.acquire()
134 try:
135 self.auth_event = event
136 self.auth_method = "password"
137 self.username = username
138 self.password = password
139 self._request_auth()
140 finally:
141 self.transport.lock.release()
143 def auth_interactive(self, username, handler, event, submethods=""):
144 """
145 response_list = handler(title, instructions, prompt_list)
146 """
147 self.transport.lock.acquire()
148 try:
149 self.auth_event = event
150 self.auth_method = "keyboard-interactive"
151 self.username = username
152 self.interactive_handler = handler
153 self.submethods = submethods
154 self._request_auth()
155 finally:
156 self.transport.lock.release()
158 def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds, event):
159 self.transport.lock.acquire()
160 try:
161 self.auth_event = event
162 self.auth_method = "gssapi-with-mic"
163 self.username = username
164 self.gss_host = gss_host
165 self.gss_deleg_creds = gss_deleg_creds
166 self._request_auth()
167 finally:
168 self.transport.lock.release()
170 def auth_gssapi_keyex(self, username, event):
171 self.transport.lock.acquire()
172 try:
173 self.auth_event = event
174 self.auth_method = "gssapi-keyex"
175 self.username = username
176 self._request_auth()
177 finally:
178 self.transport.lock.release()
180 def abort(self):
181 if self.auth_event is not None:
182 self.auth_event.set()
184 # ...internals...
186 def _request_auth(self):
187 m = Message()
188 m.add_byte(cMSG_SERVICE_REQUEST)
189 m.add_string("ssh-userauth")
190 self.transport._send_message(m)
192 def _disconnect_service_not_available(self):
193 m = Message()
194 m.add_byte(cMSG_DISCONNECT)
195 m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE)
196 m.add_string("Service not available")
197 m.add_string("en")
198 self.transport._send_message(m)
199 self.transport.close()
201 def _disconnect_no_more_auth(self):
202 m = Message()
203 m.add_byte(cMSG_DISCONNECT)
204 m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
205 m.add_string("No more auth methods available")
206 m.add_string("en")
207 self.transport._send_message(m)
208 self.transport.close()
210 def _get_key_type_and_bits(self, key):
211 """
212 Given any key, return its type/algorithm & bits-to-sign.
214 Intended for input to or verification of, key signatures.
215 """
216 # Use certificate contents, if available, plain pubkey otherwise
217 if key.public_blob:
218 return key.public_blob.key_type, key.public_blob.key_blob
219 else:
220 return key.get_name(), key
222 def _get_session_blob(self, key, service, username, algorithm):
223 m = Message()
224 m.add_string(self.transport.session_id)
225 m.add_byte(cMSG_USERAUTH_REQUEST)
226 m.add_string(username)
227 m.add_string(service)
228 m.add_string("publickey")
229 m.add_boolean(True)
230 _, bits = self._get_key_type_and_bits(key)
231 m.add_string(algorithm)
232 m.add_string(bits)
233 return m.asbytes()
235 def wait_for_response(self, event):
236 max_ts = None
237 if self.transport.auth_timeout is not None:
238 max_ts = time.time() + self.transport.auth_timeout
239 while True:
240 event.wait(0.1)
241 if not self.transport.is_active():
242 e = self.transport.get_exception()
243 if (e is None) or issubclass(e.__class__, EOFError):
244 e = AuthenticationException("Authentication failed.")
245 raise e
246 if event.is_set():
247 break
248 if max_ts is not None and max_ts <= time.time():
249 raise AuthenticationException("Authentication timeout.")
251 if not self.is_authenticated():
252 e = self.transport.get_exception()
253 if e is None:
254 e = AuthenticationException("Authentication failed.")
255 # this is horrible. Python Exception isn't yet descended from
256 # object, so type(e) won't work. :(
257 if issubclass(e.__class__, PartialAuthentication):
258 return e.allowed_types
259 raise e
260 return []
262 def _parse_service_request(self, m):
263 service = m.get_text()
264 if self.transport.server_mode and (service == "ssh-userauth"):
265 # accepted
266 m = Message()
267 m.add_byte(cMSG_SERVICE_ACCEPT)
268 m.add_string(service)
269 self.transport._send_message(m)
270 banner, language = self.transport.server_object.get_banner()
271 if banner:
272 m = Message()
273 m.add_byte(cMSG_USERAUTH_BANNER)
274 m.add_string(banner)
275 m.add_string(language)
276 self.transport._send_message(m)
277 return
278 # dunno this one
279 self._disconnect_service_not_available()
281 def _generate_key_from_request(self, algorithm, keyblob):
282 # For use in server mode.
283 options = self.transport.preferred_pubkeys
284 if algorithm.replace("-cert-v01@openssh.com", "") not in options:
285 err = (
286 "Auth rejected: pubkey algorithm '{}' unsupported or disabled"
287 )
288 self._log(INFO, err.format(algorithm))
289 return None
290 return self.transport._key_info[algorithm](Message(keyblob))
292 def _finalize_pubkey_algorithm(self, key_type):
293 # Short-circuit for non-RSA keys
294 if "rsa" not in key_type:
295 return key_type
296 self._log(
297 DEBUG,
298 "Finalizing pubkey algorithm for key of type {!r}".format(
299 key_type
300 ),
301 )
302 # NOTE re #2017: When the key is an RSA cert and the remote server is
303 # OpenSSH 7.7 or earlier, always use ssh-rsa-cert-v01@openssh.com.
304 # Those versions of the server won't support rsa-sha2 family sig algos
305 # for certs specifically, and in tandem with various server bugs
306 # regarding server-sig-algs, it's impossible to fit this into the rest
307 # of the logic here.
308 if key_type.endswith("-cert-v01@openssh.com") and re.search(
309 r"-OpenSSH_(?:[1-6]|7\.[0-7])", self.transport.remote_version
310 ):
311 pubkey_algo = "ssh-rsa-cert-v01@openssh.com"
312 self.transport._agreed_pubkey_algorithm = pubkey_algo
313 self._log(DEBUG, "OpenSSH<7.8 + RSA cert = forcing ssh-rsa!")
314 self._log(
315 DEBUG, "Agreed upon {!r} pubkey algorithm".format(pubkey_algo)
316 )
317 return pubkey_algo
318 # Normal attempts to handshake follow from here.
319 # Only consider RSA algos from our list, lest we agree on another!
320 my_algos = [x for x in self.transport.preferred_pubkeys if "rsa" in x]
321 self._log(DEBUG, "Our pubkey algorithm list: {}".format(my_algos))
322 # Short-circuit negatively if user disabled all RSA algos (heh)
323 if not my_algos:
324 raise SSHException(
325 "An RSA key was specified, but no RSA pubkey algorithms are configured!" # noqa
326 )
327 # Check for server-sig-algs if supported & sent
328 server_algo_str = u(
329 self.transport.server_extensions.get("server-sig-algs", b(""))
330 )
331 pubkey_algo = None
332 if server_algo_str:
333 server_algos = server_algo_str.split(",")
334 self._log(
335 DEBUG, "Server-side algorithm list: {}".format(server_algos)
336 )
337 # Only use algos from our list that the server likes, in our own
338 # preference order. (NOTE: purposefully using same style as in
339 # Transport...expect to refactor later)
340 agreement = list(filter(server_algos.__contains__, my_algos))
341 if agreement:
342 pubkey_algo = agreement[0]
343 self._log(
344 DEBUG,
345 "Agreed upon {!r} pubkey algorithm".format(pubkey_algo),
346 )
347 else:
348 self._log(DEBUG, "No common pubkey algorithms exist! Dying.")
349 # TODO: MAY want to use IncompatiblePeer again here but that's
350 # technically for initial key exchange, not pubkey auth.
351 err = "Unable to agree on a pubkey algorithm for signing a {!r} key!" # noqa
352 raise AuthenticationException(err.format(key_type))
353 else:
354 # Fallback: first one in our (possibly tweaked by caller) list
355 pubkey_algo = my_algos[0]
356 msg = "Server did not send a server-sig-algs list; defaulting to our first preferred algo ({!r})" # noqa
357 self._log(DEBUG, msg.format(pubkey_algo))
358 self._log(
359 DEBUG,
360 "NOTE: you may use the 'disabled_algorithms' SSHClient/Transport init kwarg to disable that or other algorithms if your server does not support them!", # noqa
361 )
362 if key_type.endswith("-cert-v01@openssh.com"):
363 pubkey_algo += "-cert-v01@openssh.com"
364 self.transport._agreed_pubkey_algorithm = pubkey_algo
365 return pubkey_algo
367 def _parse_service_accept(self, m):
368 service = m.get_text()
369 if service == "ssh-userauth":
370 # TODO 4.0: this message sucks ass. change it to something more
371 # obvious. it always appears to mean "we already authed" but no! it
372 # just means "we are allowed to TRY authing!"
373 self._log(DEBUG, "userauth is OK")
374 m = Message()
375 m.add_byte(cMSG_USERAUTH_REQUEST)
376 m.add_string(self.username)
377 m.add_string("ssh-connection")
378 m.add_string(self.auth_method)
379 if self.auth_method == "password":
380 m.add_boolean(False)
381 password = b(self.password)
382 m.add_string(password)
383 elif self.auth_method == "publickey":
384 m.add_boolean(True)
385 key_type, bits = self._get_key_type_and_bits(self.private_key)
386 algorithm = self._finalize_pubkey_algorithm(key_type)
387 m.add_string(algorithm)
388 m.add_string(bits)
389 blob = self._get_session_blob(
390 self.private_key,
391 "ssh-connection",
392 self.username,
393 algorithm,
394 )
395 sig = self.private_key.sign_ssh_data(blob, algorithm)
396 m.add_string(sig)
397 elif self.auth_method == "keyboard-interactive":
398 m.add_string("")
399 m.add_string(self.submethods)
400 elif self.auth_method == "gssapi-with-mic":
401 sshgss = GSSAuth(self.auth_method, self.gss_deleg_creds)
402 m.add_bytes(sshgss.ssh_gss_oids())
403 # send the supported GSSAPI OIDs to the server
404 self.transport._send_message(m)
405 ptype, m = self.transport.packetizer.read_message()
406 if ptype == MSG_USERAUTH_BANNER:
407 self._parse_userauth_banner(m)
408 ptype, m = self.transport.packetizer.read_message()
409 if ptype == MSG_USERAUTH_GSSAPI_RESPONSE:
410 # Read the mechanism selected by the server. We send just
411 # the Kerberos V5 OID, so the server can only respond with
412 # this OID.
413 mech = m.get_string()
414 m = Message()
415 m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
416 try:
417 m.add_string(
418 sshgss.ssh_init_sec_context(
419 self.gss_host, mech, self.username
420 )
421 )
422 except GSS_EXCEPTIONS as e:
423 return self._handle_local_gss_failure(e)
424 self.transport._send_message(m)
425 while True:
426 ptype, m = self.transport.packetizer.read_message()
427 if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
428 srv_token = m.get_string()
429 try:
430 next_token = sshgss.ssh_init_sec_context(
431 self.gss_host,
432 mech,
433 self.username,
434 srv_token,
435 )
436 except GSS_EXCEPTIONS as e:
437 return self._handle_local_gss_failure(e)
438 # After this step the GSSAPI should not return any
439 # token. If it does, we keep sending the token to
440 # the server until no more token is returned.
441 if next_token is None:
442 break
443 else:
444 m = Message()
445 m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
446 m.add_string(next_token)
447 self.transport.send_message(m)
448 else:
449 raise SSHException(
450 "Received Package: {}".format(MSG_NAMES[ptype])
451 )
452 m = Message()
453 m.add_byte(cMSG_USERAUTH_GSSAPI_MIC)
454 # send the MIC to the server
455 m.add_string(sshgss.ssh_get_mic(self.transport.session_id))
456 elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK:
457 # RFC 4462 says we are not required to implement GSS-API
458 # error messages.
459 # See RFC 4462 Section 3.8 in
460 # http://www.ietf.org/rfc/rfc4462.txt
461 raise SSHException("Server returned an error token")
462 elif ptype == MSG_USERAUTH_GSSAPI_ERROR:
463 maj_status = m.get_int()
464 min_status = m.get_int()
465 err_msg = m.get_string()
466 m.get_string() # Lang tag - discarded
467 raise SSHException(
468 """GSS-API Error:
469Major Status: {}
470Minor Status: {}
471Error Message: {}
472""".format(
473 maj_status, min_status, err_msg
474 )
475 )
476 elif ptype == MSG_USERAUTH_FAILURE:
477 self._parse_userauth_failure(m)
478 return
479 else:
480 raise SSHException(
481 "Received Package: {}".format(MSG_NAMES[ptype])
482 )
483 elif (
484 self.auth_method == "gssapi-keyex"
485 and self.transport.gss_kex_used
486 ):
487 kexgss = self.transport.kexgss_ctxt
488 kexgss.set_username(self.username)
489 mic_token = kexgss.ssh_get_mic(self.transport.session_id)
490 m.add_string(mic_token)
491 elif self.auth_method == "none":
492 pass
493 else:
494 raise SSHException(
495 'Unknown auth method "{}"'.format(self.auth_method)
496 )
497 self.transport._send_message(m)
498 else:
499 self._log(
500 DEBUG, 'Service request "{}" accepted (?)'.format(service)
501 )
503 def _send_auth_result(self, username, method, result):
504 # okay, send result
505 m = Message()
506 if result == AUTH_SUCCESSFUL:
507 self._log(INFO, "Auth granted ({}).".format(method))
508 m.add_byte(cMSG_USERAUTH_SUCCESS)
509 self.authenticated = True
510 else:
511 self._log(INFO, "Auth rejected ({}).".format(method))
512 m.add_byte(cMSG_USERAUTH_FAILURE)
513 m.add_string(
514 self.transport.server_object.get_allowed_auths(username)
515 )
516 if result == AUTH_PARTIALLY_SUCCESSFUL:
517 m.add_boolean(True)
518 else:
519 m.add_boolean(False)
520 self.auth_fail_count += 1
521 self.transport._send_message(m)
522 if self.auth_fail_count >= 10:
523 self._disconnect_no_more_auth()
524 if result == AUTH_SUCCESSFUL:
525 self.transport._auth_trigger()
527 def _interactive_query(self, q):
528 # make interactive query instead of response
529 m = Message()
530 m.add_byte(cMSG_USERAUTH_INFO_REQUEST)
531 m.add_string(q.name)
532 m.add_string(q.instructions)
533 m.add_string(bytes())
534 m.add_int(len(q.prompts))
535 for p in q.prompts:
536 m.add_string(p[0])
537 m.add_boolean(p[1])
538 self.transport._send_message(m)
540 def _parse_userauth_request(self, m):
541 if not self.transport.server_mode:
542 # er, uh... what?
543 m = Message()
544 m.add_byte(cMSG_USERAUTH_FAILURE)
545 m.add_string("none")
546 m.add_boolean(False)
547 self.transport._send_message(m)
548 return
549 if self.authenticated:
550 # ignore
551 return
552 username = m.get_text()
553 service = m.get_text()
554 method = m.get_text()
555 self._log(
556 DEBUG,
557 "Auth request (type={}) service={}, username={}".format(
558 method, service, username
559 ),
560 )
561 if service != "ssh-connection":
562 self._disconnect_service_not_available()
563 return
564 if (self.auth_username is not None) and (
565 self.auth_username != username
566 ):
567 self._log(
568 WARNING,
569 "Auth rejected because the client attempted to change username in mid-flight", # noqa
570 )
571 self._disconnect_no_more_auth()
572 return
573 self.auth_username = username
574 # check if GSS-API authentication is enabled
575 gss_auth = self.transport.server_object.enable_auth_gssapi()
577 if method == "none":
578 result = self.transport.server_object.check_auth_none(username)
579 elif method == "password":
580 changereq = m.get_boolean()
581 password = m.get_binary()
582 try:
583 password = password.decode("UTF-8")
584 except UnicodeError:
585 # some clients/servers expect non-utf-8 passwords!
586 # in this case, just return the raw byte string.
587 pass
588 if changereq:
589 # always treated as failure, since we don't support changing
590 # passwords, but collect the list of valid auth types from
591 # the callback anyway
592 self._log(DEBUG, "Auth request to change passwords (rejected)")
593 newpassword = m.get_binary()
594 try:
595 newpassword = newpassword.decode("UTF-8", "replace")
596 except UnicodeError:
597 pass
598 result = AUTH_FAILED
599 else:
600 result = self.transport.server_object.check_auth_password(
601 username, password
602 )
603 elif method == "publickey":
604 sig_attached = m.get_boolean()
605 # NOTE: server never wants to guess a client's algo, they're
606 # telling us directly. No need for _finalize_pubkey_algorithm
607 # anywhere in this flow.
608 algorithm = m.get_text()
609 keyblob = m.get_binary()
610 try:
611 key = self._generate_key_from_request(algorithm, keyblob)
612 except SSHException as e:
613 self._log(INFO, "Auth rejected: public key: {}".format(str(e)))
614 key = None
615 except Exception as e:
616 msg = "Auth rejected: unsupported or mangled public key ({}: {})" # noqa
617 self._log(INFO, msg.format(e.__class__.__name__, e))
618 key = None
619 if key is None:
620 self._disconnect_no_more_auth()
621 return
622 # first check if this key is okay... if not, we can skip the verify
623 result = self.transport.server_object.check_auth_publickey(
624 username, key
625 )
626 if result != AUTH_FAILED:
627 # key is okay, verify it
628 if not sig_attached:
629 # client wants to know if this key is acceptable, before it
630 # signs anything... send special "ok" message
631 m = Message()
632 m.add_byte(cMSG_USERAUTH_PK_OK)
633 m.add_string(algorithm)
634 m.add_string(keyblob)
635 self.transport._send_message(m)
636 return
637 sig = Message(m.get_binary())
638 blob = self._get_session_blob(
639 key, service, username, algorithm
640 )
641 if not key.verify_ssh_sig(blob, sig):
642 self._log(INFO, "Auth rejected: invalid signature")
643 result = AUTH_FAILED
644 elif method == "keyboard-interactive":
645 submethods = m.get_string()
646 result = self.transport.server_object.check_auth_interactive(
647 username, submethods
648 )
649 if isinstance(result, InteractiveQuery):
650 # make interactive query instead of response
651 self._interactive_query(result)
652 return
653 elif method == "gssapi-with-mic" and gss_auth:
654 sshgss = GSSAuth(method)
655 # Read the number of OID mechanisms supported by the client.
656 # OpenSSH sends just one OID. It's the Kerveros V5 OID and that's
657 # the only OID we support.
658 mechs = m.get_int()
659 # We can't accept more than one OID, so if the SSH client sends
660 # more than one, disconnect.
661 if mechs > 1:
662 self._log(
663 INFO,
664 "Disconnect: Received more than one GSS-API OID mechanism",
665 )
666 self._disconnect_no_more_auth()
667 desired_mech = m.get_string()
668 mech_ok = sshgss.ssh_check_mech(desired_mech)
669 # if we don't support the mechanism, disconnect.
670 if not mech_ok:
671 self._log(
672 INFO,
673 "Disconnect: Received an invalid GSS-API OID mechanism",
674 )
675 self._disconnect_no_more_auth()
676 # send the Kerberos V5 GSSAPI OID to the client
677 supported_mech = sshgss.ssh_gss_oids("server")
678 # RFC 4462 says we are not required to implement GSS-API error
679 # messages. See section 3.8 in http://www.ietf.org/rfc/rfc4462.txt
680 m = Message()
681 m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE)
682 m.add_bytes(supported_mech)
683 self.transport.auth_handler = GssapiWithMicAuthHandler(
684 self, sshgss
685 )
686 self.transport._expected_packet = (
687 MSG_USERAUTH_GSSAPI_TOKEN,
688 MSG_USERAUTH_REQUEST,
689 MSG_SERVICE_REQUEST,
690 )
691 self.transport._send_message(m)
692 return
693 elif method == "gssapi-keyex" and gss_auth:
694 mic_token = m.get_string()
695 sshgss = self.transport.kexgss_ctxt
696 if sshgss is None:
697 # If there is no valid context, we reject the authentication
698 result = AUTH_FAILED
699 self._send_auth_result(username, method, result)
700 try:
701 sshgss.ssh_check_mic(
702 mic_token, self.transport.session_id, self.auth_username
703 )
704 except Exception:
705 result = AUTH_FAILED
706 self._send_auth_result(username, method, result)
707 raise
708 result = AUTH_SUCCESSFUL
709 self.transport.server_object.check_auth_gssapi_keyex(
710 username, result
711 )
712 else:
713 result = self.transport.server_object.check_auth_none(username)
714 # okay, send result
715 self._send_auth_result(username, method, result)
717 def _parse_userauth_success(self, m):
718 self._log(
719 INFO, "Authentication ({}) successful!".format(self.auth_method)
720 )
721 self.authenticated = True
722 self.transport._auth_trigger()
723 if self.auth_event is not None:
724 self.auth_event.set()
726 def _parse_userauth_failure(self, m):
727 authlist = m.get_list()
728 partial = m.get_boolean()
729 if partial:
730 self._log(INFO, "Authentication continues...")
731 self._log(DEBUG, "Methods: " + str(authlist))
732 self.transport.saved_exception = PartialAuthentication(authlist)
733 elif self.auth_method not in authlist:
734 for msg in (
735 "Authentication type ({}) not permitted.".format(
736 self.auth_method
737 ),
738 "Allowed methods: {}".format(authlist),
739 ):
740 self._log(DEBUG, msg)
741 self.transport.saved_exception = BadAuthenticationType(
742 "Bad authentication type", authlist
743 )
744 else:
745 self._log(
746 INFO, "Authentication ({}) failed.".format(self.auth_method)
747 )
748 self.authenticated = False
749 self.username = None
750 if self.auth_event is not None:
751 self.auth_event.set()
753 def _parse_userauth_banner(self, m):
754 banner = m.get_string()
755 self.banner = banner
756 self._log(INFO, "Auth banner: {}".format(banner))
757 # who cares.
759 def _parse_userauth_info_request(self, m):
760 if self.auth_method != "keyboard-interactive":
761 raise SSHException("Illegal info request from server")
762 title = m.get_text()
763 instructions = m.get_text()
764 m.get_binary() # lang
765 prompts = m.get_int()
766 prompt_list = []
767 for i in range(prompts):
768 prompt_list.append((m.get_text(), m.get_boolean()))
769 response_list = self.interactive_handler(
770 title, instructions, prompt_list
771 )
773 m = Message()
774 m.add_byte(cMSG_USERAUTH_INFO_RESPONSE)
775 m.add_int(len(response_list))
776 for r in response_list:
777 m.add_string(r)
778 self.transport._send_message(m)
780 def _parse_userauth_info_response(self, m):
781 if not self.transport.server_mode:
782 raise SSHException("Illegal info response from server")
783 n = m.get_int()
784 responses = []
785 for i in range(n):
786 responses.append(m.get_text())
787 result = self.transport.server_object.check_auth_interactive_response(
788 responses
789 )
790 if isinstance(result, InteractiveQuery):
791 # make interactive query instead of response
792 self._interactive_query(result)
793 return
794 self._send_auth_result(
795 self.auth_username, "keyboard-interactive", result
796 )
798 def _handle_local_gss_failure(self, e):
799 self.transport.saved_exception = e
800 self._log(DEBUG, "GSSAPI failure: {}".format(e))
801 self._log(INFO, "Authentication ({}) failed.".format(self.auth_method))
802 self.authenticated = False
803 self.username = None
804 if self.auth_event is not None:
805 self.auth_event.set()
806 return
808 # TODO: do the same to the other tables, in Transport.
809 # TODO 4.0: MAY make sense to make these tables into actual
810 # classes/instances that can be fed a mode bool or whatever. Or,
811 # alternately (both?) make the message types small classes or enums that
812 # embed this info within themselves (which could also then tidy up the
813 # current 'integer -> human readable short string' stuff in common.py).
814 # TODO: if we do that, also expose 'em publicly.
816 # Messages which should be handled _by_ servers (sent by clients)
817 _server_handler_table = {
818 MSG_SERVICE_REQUEST: _parse_service_request,
819 MSG_USERAUTH_REQUEST: _parse_userauth_request,
820 MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response,
821 }
823 # Messages which should be handled _by_ clients (sent by servers)
824 _client_handler_table = {
825 MSG_SERVICE_ACCEPT: _parse_service_accept,
826 MSG_USERAUTH_SUCCESS: _parse_userauth_success,
827 MSG_USERAUTH_FAILURE: _parse_userauth_failure,
828 MSG_USERAUTH_BANNER: _parse_userauth_banner,
829 MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request,
830 }
832 # NOTE: prior to the fix for #1283, this was a static dict instead of a
833 # property. Should be backwards compatible in most/all cases.
834 @property
835 def _handler_table(self):
836 if self.transport.server_mode:
837 return self._server_handler_table
838 else:
839 return self._client_handler_table
842class GssapiWithMicAuthHandler:
843 """A specialized Auth handler for gssapi-with-mic
845 During the GSSAPI token exchange we need a modified dispatch table,
846 because the packet type numbers are not unique.
847 """
849 method = "gssapi-with-mic"
851 def __init__(self, delegate, sshgss):
852 self._delegate = delegate
853 self.sshgss = sshgss
855 def abort(self):
856 self._restore_delegate_auth_handler()
857 return self._delegate.abort()
859 @property
860 def transport(self):
861 return self._delegate.transport
863 @property
864 def _send_auth_result(self):
865 return self._delegate._send_auth_result
867 @property
868 def auth_username(self):
869 return self._delegate.auth_username
871 @property
872 def gss_host(self):
873 return self._delegate.gss_host
875 def _restore_delegate_auth_handler(self):
876 self.transport.auth_handler = self._delegate
878 def _parse_userauth_gssapi_token(self, m):
879 client_token = m.get_string()
880 # use the client token as input to establish a secure
881 # context.
882 sshgss = self.sshgss
883 try:
884 token = sshgss.ssh_accept_sec_context(
885 self.gss_host, client_token, self.auth_username
886 )
887 except Exception as e:
888 self.transport.saved_exception = e
889 result = AUTH_FAILED
890 self._restore_delegate_auth_handler()
891 self._send_auth_result(self.auth_username, self.method, result)
892 raise
893 if token is not None:
894 m = Message()
895 m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
896 m.add_string(token)
897 self.transport._expected_packet = (
898 MSG_USERAUTH_GSSAPI_TOKEN,
899 MSG_USERAUTH_GSSAPI_MIC,
900 MSG_USERAUTH_REQUEST,
901 )
902 self.transport._send_message(m)
904 def _parse_userauth_gssapi_mic(self, m):
905 mic_token = m.get_string()
906 sshgss = self.sshgss
907 username = self.auth_username
908 self._restore_delegate_auth_handler()
909 try:
910 sshgss.ssh_check_mic(
911 mic_token, self.transport.session_id, username
912 )
913 except Exception as e:
914 self.transport.saved_exception = e
915 result = AUTH_FAILED
916 self._send_auth_result(username, self.method, result)
917 raise
918 # TODO: Implement client credential saving.
919 # The OpenSSH server is able to create a TGT with the delegated
920 # client credentials, but this is not supported by GSS-API.
921 result = AUTH_SUCCESSFUL
922 self.transport.server_object.check_auth_gssapi_with_mic(
923 username, result
924 )
925 # okay, send result
926 self._send_auth_result(username, self.method, result)
928 def _parse_service_request(self, m):
929 self._restore_delegate_auth_handler()
930 return self._delegate._parse_service_request(m)
932 def _parse_userauth_request(self, m):
933 self._restore_delegate_auth_handler()
934 return self._delegate._parse_userauth_request(m)
936 __handler_table = {
937 MSG_SERVICE_REQUEST: _parse_service_request,
938 MSG_USERAUTH_REQUEST: _parse_userauth_request,
939 MSG_USERAUTH_GSSAPI_TOKEN: _parse_userauth_gssapi_token,
940 MSG_USERAUTH_GSSAPI_MIC: _parse_userauth_gssapi_mic,
941 }
943 @property
944 def _handler_table(self):
945 # TODO: determine if we can cut this up like we did for the primary
946 # AuthHandler class.
947 return self.__handler_table