Coverage for /pythoncovmergedfiles/medio/medio/src/paramiko/paramiko/auth_handler.py: 12%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 threading
25import time
26import re
28from paramiko.common import (
29 cMSG_SERVICE_REQUEST,
30 cMSG_DISCONNECT,
31 DISCONNECT_SERVICE_NOT_AVAILABLE,
32 DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
33 cMSG_USERAUTH_REQUEST,
34 cMSG_SERVICE_ACCEPT,
35 DEBUG,
36 AUTH_SUCCESSFUL,
37 INFO,
38 cMSG_USERAUTH_SUCCESS,
39 cMSG_USERAUTH_FAILURE,
40 AUTH_PARTIALLY_SUCCESSFUL,
41 cMSG_USERAUTH_INFO_REQUEST,
42 WARNING,
43 AUTH_FAILED,
44 cMSG_USERAUTH_PK_OK,
45 cMSG_USERAUTH_INFO_RESPONSE,
46 MSG_SERVICE_REQUEST,
47 MSG_SERVICE_ACCEPT,
48 MSG_USERAUTH_REQUEST,
49 MSG_USERAUTH_SUCCESS,
50 MSG_USERAUTH_FAILURE,
51 MSG_USERAUTH_BANNER,
52 MSG_USERAUTH_INFO_REQUEST,
53 MSG_USERAUTH_INFO_RESPONSE,
54 cMSG_USERAUTH_GSSAPI_RESPONSE,
55 cMSG_USERAUTH_GSSAPI_TOKEN,
56 cMSG_USERAUTH_GSSAPI_MIC,
57 MSG_USERAUTH_GSSAPI_RESPONSE,
58 MSG_USERAUTH_GSSAPI_TOKEN,
59 MSG_USERAUTH_GSSAPI_ERROR,
60 MSG_USERAUTH_GSSAPI_ERRTOK,
61 MSG_USERAUTH_GSSAPI_MIC,
62 MSG_NAMES,
63 cMSG_USERAUTH_BANNER,
64)
65from paramiko.message import Message
66from paramiko.util import b, u
67from paramiko.ssh_exception import (
68 SSHException,
69 AuthenticationException,
70 BadAuthenticationType,
71 PartialAuthentication,
72)
73from paramiko.server import InteractiveQuery
74from paramiko.ssh_gss import GSSAuth, GSS_EXCEPTIONS
77class AuthHandler:
78 """
79 Internal class to handle the mechanics of authentication.
80 """
82 def __init__(self, transport):
83 self.transport = weakref.proxy(transport)
84 self.username = None
85 self.authenticated = False
86 self.auth_event = None
87 self.auth_method = ""
88 self.banner = None
89 self.password = None
90 self.private_key = None
91 self.interactive_handler = None
92 self.submethods = None
93 # for server mode:
94 self.auth_username = None
95 self.auth_fail_count = 0
96 # for GSSAPI
97 self.gss_host = None
98 self.gss_deleg_creds = True
100 def _log(self, *args):
101 return self.transport._log(*args)
103 def is_authenticated(self):
104 return self.authenticated
106 def get_username(self):
107 if self.transport.server_mode:
108 return self.auth_username
109 else:
110 return self.username
112 def auth_none(self, username, event):
113 self.transport.lock.acquire()
114 try:
115 self.auth_event = event
116 self.auth_method = "none"
117 self.username = username
118 self._request_auth()
119 finally:
120 self.transport.lock.release()
122 def auth_publickey(self, username, key, event):
123 self.transport.lock.acquire()
124 try:
125 self.auth_event = event
126 self.auth_method = "publickey"
127 self.username = username
128 self.private_key = key
129 self._request_auth()
130 finally:
131 self.transport.lock.release()
133 def auth_password(self, username, password, event):
134 self.transport.lock.acquire()
135 try:
136 self.auth_event = event
137 self.auth_method = "password"
138 self.username = username
139 self.password = password
140 self._request_auth()
141 finally:
142 self.transport.lock.release()
144 def auth_interactive(self, username, handler, event, submethods=""):
145 """
146 response_list = handler(title, instructions, prompt_list)
147 """
148 self.transport.lock.acquire()
149 try:
150 self.auth_event = event
151 self.auth_method = "keyboard-interactive"
152 self.username = username
153 self.interactive_handler = handler
154 self.submethods = submethods
155 self._request_auth()
156 finally:
157 self.transport.lock.release()
159 def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds, event):
160 self.transport.lock.acquire()
161 try:
162 self.auth_event = event
163 self.auth_method = "gssapi-with-mic"
164 self.username = username
165 self.gss_host = gss_host
166 self.gss_deleg_creds = gss_deleg_creds
167 self._request_auth()
168 finally:
169 self.transport.lock.release()
171 def auth_gssapi_keyex(self, username, event):
172 self.transport.lock.acquire()
173 try:
174 self.auth_event = event
175 self.auth_method = "gssapi-keyex"
176 self.username = username
177 self._request_auth()
178 finally:
179 self.transport.lock.release()
181 def abort(self):
182 if self.auth_event is not None:
183 self.auth_event.set()
185 # ...internals...
187 def _request_auth(self):
188 m = Message()
189 m.add_byte(cMSG_SERVICE_REQUEST)
190 m.add_string("ssh-userauth")
191 self.transport._send_message(m)
193 def _disconnect_service_not_available(self):
194 m = Message()
195 m.add_byte(cMSG_DISCONNECT)
196 m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE)
197 m.add_string("Service not available")
198 m.add_string("en")
199 self.transport._send_message(m)
200 self.transport.close()
202 def _disconnect_no_more_auth(self):
203 m = Message()
204 m.add_byte(cMSG_DISCONNECT)
205 m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
206 m.add_string("No more auth methods available")
207 m.add_string("en")
208 self.transport._send_message(m)
209 self.transport.close()
211 def _get_key_type_and_bits(self, key):
212 """
213 Given any key, return its type/algorithm & bits-to-sign.
215 Intended for input to or verification of, key signatures.
216 """
217 # Use certificate contents, if available, plain pubkey otherwise
218 if key.public_blob:
219 return key.public_blob.key_type, key.public_blob.key_blob
220 else:
221 return key.get_name(), key
223 def _get_session_blob(self, key, service, username, algorithm):
224 m = Message()
225 m.add_string(self.transport.session_id)
226 m.add_byte(cMSG_USERAUTH_REQUEST)
227 m.add_string(username)
228 m.add_string(service)
229 m.add_string("publickey")
230 m.add_boolean(True)
231 _, bits = self._get_key_type_and_bits(key)
232 m.add_string(algorithm)
233 m.add_string(bits)
234 return m.asbytes()
236 def wait_for_response(self, event):
237 max_ts = None
238 if self.transport.auth_timeout is not None:
239 max_ts = time.time() + self.transport.auth_timeout
240 while True:
241 event.wait(0.1)
242 if not self.transport.is_active():
243 e = self.transport.get_exception()
244 if (e is None) or issubclass(e.__class__, EOFError):
245 e = AuthenticationException(
246 "Authentication failed: transport shut down or saw EOF"
247 )
248 raise e
249 if event.is_set():
250 break
251 if max_ts is not None and max_ts <= time.time():
252 raise AuthenticationException("Authentication timeout.")
254 if not self.is_authenticated():
255 e = self.transport.get_exception()
256 if e is None:
257 e = AuthenticationException("Authentication failed.")
258 # this is horrible. Python Exception isn't yet descended from
259 # object, so type(e) won't work. :(
260 # TODO 4.0: lol. just lmao.
261 if issubclass(e.__class__, PartialAuthentication):
262 return e.allowed_types
263 raise e
264 return []
266 def _parse_service_request(self, m):
267 service = m.get_text()
268 if self.transport.server_mode and (service == "ssh-userauth"):
269 # accepted
270 m = Message()
271 m.add_byte(cMSG_SERVICE_ACCEPT)
272 m.add_string(service)
273 self.transport._send_message(m)
274 banner, language = self.transport.server_object.get_banner()
275 if banner:
276 m = Message()
277 m.add_byte(cMSG_USERAUTH_BANNER)
278 m.add_string(banner)
279 m.add_string(language)
280 self.transport._send_message(m)
281 return
282 # dunno this one
283 self._disconnect_service_not_available()
285 def _generate_key_from_request(self, algorithm, keyblob):
286 # For use in server mode.
287 options = self.transport.preferred_pubkeys
288 if algorithm.replace("-cert-v01@openssh.com", "") not in options:
289 err = (
290 "Auth rejected: pubkey algorithm '{}' unsupported or disabled"
291 )
292 self._log(INFO, err.format(algorithm))
293 return None
294 return self.transport._key_info[algorithm](Message(keyblob))
296 def _choose_fallback_pubkey_algorithm(self, key_type, my_algos):
297 # Fallback: first one in our (possibly tweaked by caller) list
298 pubkey_algo = my_algos[0]
299 msg = "Server did not send a server-sig-algs list; defaulting to our first preferred algo ({!r})" # noqa
300 self._log(DEBUG, msg.format(pubkey_algo))
301 self._log(
302 DEBUG,
303 "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
304 )
305 return pubkey_algo
307 def _finalize_pubkey_algorithm(self, key_type):
308 # Short-circuit for non-RSA keys
309 if "rsa" not in key_type:
310 return key_type
311 self._log(
312 DEBUG,
313 "Finalizing pubkey algorithm for key of type {!r}".format(
314 key_type
315 ),
316 )
317 # NOTE re #2017: When the key is an RSA cert and the remote server is
318 # OpenSSH 7.7 or earlier, always use ssh-rsa-cert-v01@openssh.com.
319 # Those versions of the server won't support rsa-sha2 family sig algos
320 # for certs specifically, and in tandem with various server bugs
321 # regarding server-sig-algs, it's impossible to fit this into the rest
322 # of the logic here.
323 if key_type.endswith("-cert-v01@openssh.com") and re.search(
324 r"-OpenSSH_(?:[1-6]|7\.[0-7])", self.transport.remote_version
325 ):
326 pubkey_algo = "ssh-rsa-cert-v01@openssh.com"
327 self.transport._agreed_pubkey_algorithm = pubkey_algo
328 self._log(DEBUG, "OpenSSH<7.8 + RSA cert = forcing ssh-rsa!")
329 self._log(
330 DEBUG, "Agreed upon {!r} pubkey algorithm".format(pubkey_algo)
331 )
332 return pubkey_algo
333 # Normal attempts to handshake follow from here.
334 # Only consider RSA algos from our list, lest we agree on another!
335 my_algos = [x for x in self.transport.preferred_pubkeys if "rsa" in x]
336 self._log(DEBUG, "Our pubkey algorithm list: {}".format(my_algos))
337 # Short-circuit negatively if user disabled all RSA algos (heh)
338 if not my_algos:
339 raise SSHException(
340 "An RSA key was specified, but no RSA pubkey algorithms are configured!" # noqa
341 )
342 # Check for server-sig-algs if supported & sent
343 server_algo_str = u(
344 self.transport.server_extensions.get("server-sig-algs", b(""))
345 )
346 pubkey_algo = None
347 # Prefer to match against server-sig-algs
348 if server_algo_str:
349 server_algos = server_algo_str.split(",")
350 self._log(
351 DEBUG, "Server-side algorithm list: {}".format(server_algos)
352 )
353 # Only use algos from our list that the server likes, in our own
354 # preference order. (NOTE: purposefully using same style as in
355 # Transport...expect to refactor later)
356 agreement = list(filter(server_algos.__contains__, my_algos))
357 if agreement:
358 pubkey_algo = agreement[0]
359 self._log(
360 DEBUG,
361 "Agreed upon {!r} pubkey algorithm".format(pubkey_algo),
362 )
363 else:
364 self._log(DEBUG, "No common pubkey algorithms exist! Dying.")
365 # TODO: MAY want to use IncompatiblePeer again here but that's
366 # technically for initial key exchange, not pubkey auth.
367 err = "Unable to agree on a pubkey algorithm for signing a {!r} key!" # noqa
368 raise AuthenticationException(err.format(key_type))
369 # Fallback to something based purely on the key & our configuration
370 else:
371 pubkey_algo = self._choose_fallback_pubkey_algorithm(
372 key_type, my_algos
373 )
374 if key_type.endswith("-cert-v01@openssh.com"):
375 pubkey_algo += "-cert-v01@openssh.com"
376 self.transport._agreed_pubkey_algorithm = pubkey_algo
377 return pubkey_algo
379 def _parse_service_accept(self, m):
380 service = m.get_text()
381 if service == "ssh-userauth":
382 self._log(DEBUG, "userauth is OK")
383 m = Message()
384 m.add_byte(cMSG_USERAUTH_REQUEST)
385 m.add_string(self.username)
386 m.add_string("ssh-connection")
387 m.add_string(self.auth_method)
388 if self.auth_method == "password":
389 m.add_boolean(False)
390 password = b(self.password)
391 m.add_string(password)
392 elif self.auth_method == "publickey":
393 m.add_boolean(True)
394 key_type, bits = self._get_key_type_and_bits(self.private_key)
395 algorithm = self._finalize_pubkey_algorithm(key_type)
396 m.add_string(algorithm)
397 m.add_string(bits)
398 blob = self._get_session_blob(
399 self.private_key,
400 "ssh-connection",
401 self.username,
402 algorithm,
403 )
404 sig = self.private_key.sign_ssh_data(blob, algorithm)
405 m.add_string(sig)
406 elif self.auth_method == "keyboard-interactive":
407 m.add_string("")
408 m.add_string(self.submethods)
409 elif self.auth_method == "gssapi-with-mic":
410 sshgss = GSSAuth(self.auth_method, self.gss_deleg_creds)
411 m.add_bytes(sshgss.ssh_gss_oids())
412 # send the supported GSSAPI OIDs to the server
413 self.transport._send_message(m)
414 ptype, m = self.transport.packetizer.read_message()
415 if ptype == MSG_USERAUTH_BANNER:
416 self._parse_userauth_banner(m)
417 ptype, m = self.transport.packetizer.read_message()
418 if ptype == MSG_USERAUTH_GSSAPI_RESPONSE:
419 # Read the mechanism selected by the server. We send just
420 # the Kerberos V5 OID, so the server can only respond with
421 # this OID.
422 mech = m.get_string()
423 m = Message()
424 m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
425 try:
426 m.add_string(
427 sshgss.ssh_init_sec_context(
428 self.gss_host, mech, self.username
429 )
430 )
431 except GSS_EXCEPTIONS as e:
432 return self._handle_local_gss_failure(e)
433 self.transport._send_message(m)
434 while True:
435 ptype, m = self.transport.packetizer.read_message()
436 if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
437 srv_token = m.get_string()
438 try:
439 next_token = sshgss.ssh_init_sec_context(
440 self.gss_host,
441 mech,
442 self.username,
443 srv_token,
444 )
445 except GSS_EXCEPTIONS as e:
446 return self._handle_local_gss_failure(e)
447 # After this step the GSSAPI should not return any
448 # token. If it does, we keep sending the token to
449 # the server until no more token is returned.
450 if next_token is None:
451 break
452 else:
453 m = Message()
454 m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
455 m.add_string(next_token)
456 self.transport.send_message(m)
457 else:
458 raise SSHException(
459 "Received Package: {}".format(MSG_NAMES[ptype])
460 )
461 m = Message()
462 m.add_byte(cMSG_USERAUTH_GSSAPI_MIC)
463 # send the MIC to the server
464 m.add_string(sshgss.ssh_get_mic(self.transport.session_id))
465 elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK:
466 # RFC 4462 says we are not required to implement GSS-API
467 # error messages.
468 # See RFC 4462 Section 3.8 in
469 # http://www.ietf.org/rfc/rfc4462.txt
470 raise SSHException("Server returned an error token")
471 elif ptype == MSG_USERAUTH_GSSAPI_ERROR:
472 maj_status = m.get_int()
473 min_status = m.get_int()
474 err_msg = m.get_string()
475 m.get_string() # Lang tag - discarded
476 raise SSHException(
477 """GSS-API Error:
478Major Status: {}
479Minor Status: {}
480Error Message: {}
481""".format(
482 maj_status, min_status, err_msg
483 )
484 )
485 elif ptype == MSG_USERAUTH_FAILURE:
486 self._parse_userauth_failure(m)
487 return
488 else:
489 raise SSHException(
490 "Received Package: {}".format(MSG_NAMES[ptype])
491 )
492 elif (
493 self.auth_method == "gssapi-keyex"
494 and self.transport.gss_kex_used
495 ):
496 kexgss = self.transport.kexgss_ctxt
497 kexgss.set_username(self.username)
498 mic_token = kexgss.ssh_get_mic(self.transport.session_id)
499 m.add_string(mic_token)
500 elif self.auth_method == "none":
501 pass
502 else:
503 raise SSHException(
504 'Unknown auth method "{}"'.format(self.auth_method)
505 )
506 self.transport._send_message(m)
507 else:
508 self._log(
509 DEBUG, 'Service request "{}" accepted (?)'.format(service)
510 )
512 def _send_auth_result(self, username, method, result):
513 # okay, send result
514 m = Message()
515 if result == AUTH_SUCCESSFUL:
516 self._log(INFO, "Auth granted ({}).".format(method))
517 m.add_byte(cMSG_USERAUTH_SUCCESS)
518 self.authenticated = True
519 else:
520 self._log(INFO, "Auth rejected ({}).".format(method))
521 m.add_byte(cMSG_USERAUTH_FAILURE)
522 m.add_string(
523 self.transport.server_object.get_allowed_auths(username)
524 )
525 if result == AUTH_PARTIALLY_SUCCESSFUL:
526 m.add_boolean(True)
527 else:
528 m.add_boolean(False)
529 self.auth_fail_count += 1
530 self.transport._send_message(m)
531 if self.auth_fail_count >= 10:
532 self._disconnect_no_more_auth()
533 if result == AUTH_SUCCESSFUL:
534 self.transport._auth_trigger()
536 def _interactive_query(self, q):
537 # make interactive query instead of response
538 m = Message()
539 m.add_byte(cMSG_USERAUTH_INFO_REQUEST)
540 m.add_string(q.name)
541 m.add_string(q.instructions)
542 m.add_string(bytes())
543 m.add_int(len(q.prompts))
544 for p in q.prompts:
545 m.add_string(p[0])
546 m.add_boolean(p[1])
547 self.transport._send_message(m)
549 def _parse_userauth_request(self, m):
550 if not self.transport.server_mode:
551 # er, uh... what?
552 m = Message()
553 m.add_byte(cMSG_USERAUTH_FAILURE)
554 m.add_string("none")
555 m.add_boolean(False)
556 self.transport._send_message(m)
557 return
558 if self.authenticated:
559 # ignore
560 return
561 username = m.get_text()
562 service = m.get_text()
563 method = m.get_text()
564 self._log(
565 DEBUG,
566 "Auth request (type={}) service={}, username={}".format(
567 method, service, username
568 ),
569 )
570 if service != "ssh-connection":
571 self._disconnect_service_not_available()
572 return
573 if (self.auth_username is not None) and (
574 self.auth_username != username
575 ):
576 self._log(
577 WARNING,
578 "Auth rejected because the client attempted to change username in mid-flight", # noqa
579 )
580 self._disconnect_no_more_auth()
581 return
582 self.auth_username = username
583 # check if GSS-API authentication is enabled
584 gss_auth = self.transport.server_object.enable_auth_gssapi()
586 if method == "none":
587 result = self.transport.server_object.check_auth_none(username)
588 elif method == "password":
589 changereq = m.get_boolean()
590 password = m.get_binary()
591 try:
592 password = password.decode("UTF-8")
593 except UnicodeError:
594 # some clients/servers expect non-utf-8 passwords!
595 # in this case, just return the raw byte string.
596 pass
597 if changereq:
598 # always treated as failure, since we don't support changing
599 # passwords, but collect the list of valid auth types from
600 # the callback anyway
601 self._log(DEBUG, "Auth request to change passwords (rejected)")
602 newpassword = m.get_binary()
603 try:
604 newpassword = newpassword.decode("UTF-8", "replace")
605 except UnicodeError:
606 pass
607 result = AUTH_FAILED
608 else:
609 result = self.transport.server_object.check_auth_password(
610 username, password
611 )
612 elif method == "publickey":
613 sig_attached = m.get_boolean()
614 # NOTE: server never wants to guess a client's algo, they're
615 # telling us directly. No need for _finalize_pubkey_algorithm
616 # anywhere in this flow.
617 algorithm = m.get_text()
618 keyblob = m.get_binary()
619 try:
620 key = self._generate_key_from_request(algorithm, keyblob)
621 except SSHException as e:
622 self._log(INFO, "Auth rejected: public key: {}".format(str(e)))
623 key = None
624 except Exception as e:
625 msg = "Auth rejected: unsupported or mangled public key ({}: {})" # noqa
626 self._log(INFO, msg.format(e.__class__.__name__, e))
627 key = None
628 if key is None:
629 self._disconnect_no_more_auth()
630 return
631 # first check if this key is okay... if not, we can skip the verify
632 result = self.transport.server_object.check_auth_publickey(
633 username, key
634 )
635 if result != AUTH_FAILED:
636 # key is okay, verify it
637 if not sig_attached:
638 # client wants to know if this key is acceptable, before it
639 # signs anything... send special "ok" message
640 m = Message()
641 m.add_byte(cMSG_USERAUTH_PK_OK)
642 m.add_string(algorithm)
643 m.add_string(keyblob)
644 self.transport._send_message(m)
645 return
646 sig = Message(m.get_binary())
647 blob = self._get_session_blob(
648 key, service, username, algorithm
649 )
650 if not key.verify_ssh_sig(blob, sig):
651 self._log(INFO, "Auth rejected: invalid signature")
652 result = AUTH_FAILED
653 elif method == "keyboard-interactive":
654 submethods = m.get_string()
655 result = self.transport.server_object.check_auth_interactive(
656 username, submethods
657 )
658 if isinstance(result, InteractiveQuery):
659 # make interactive query instead of response
660 self._interactive_query(result)
661 return
662 elif method == "gssapi-with-mic" and gss_auth:
663 sshgss = GSSAuth(method)
664 # Read the number of OID mechanisms supported by the client.
665 # OpenSSH sends just one OID. It's the Kerveros V5 OID and that's
666 # the only OID we support.
667 mechs = m.get_int()
668 # We can't accept more than one OID, so if the SSH client sends
669 # more than one, disconnect.
670 if mechs > 1:
671 self._log(
672 INFO,
673 "Disconnect: Received more than one GSS-API OID mechanism",
674 )
675 self._disconnect_no_more_auth()
676 desired_mech = m.get_string()
677 mech_ok = sshgss.ssh_check_mech(desired_mech)
678 # if we don't support the mechanism, disconnect.
679 if not mech_ok:
680 self._log(
681 INFO,
682 "Disconnect: Received an invalid GSS-API OID mechanism",
683 )
684 self._disconnect_no_more_auth()
685 # send the Kerberos V5 GSSAPI OID to the client
686 supported_mech = sshgss.ssh_gss_oids("server")
687 # RFC 4462 says we are not required to implement GSS-API error
688 # messages. See section 3.8 in http://www.ietf.org/rfc/rfc4462.txt
689 m = Message()
690 m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE)
691 m.add_bytes(supported_mech)
692 self.transport.auth_handler = GssapiWithMicAuthHandler(
693 self, sshgss
694 )
695 self.transport._expected_packet = (
696 MSG_USERAUTH_GSSAPI_TOKEN,
697 MSG_USERAUTH_REQUEST,
698 MSG_SERVICE_REQUEST,
699 )
700 self.transport._send_message(m)
701 return
702 elif method == "gssapi-keyex" and gss_auth:
703 mic_token = m.get_string()
704 sshgss = self.transport.kexgss_ctxt
705 if sshgss is None:
706 # If there is no valid context, we reject the authentication
707 result = AUTH_FAILED
708 self._send_auth_result(username, method, result)
709 try:
710 sshgss.ssh_check_mic(
711 mic_token, self.transport.session_id, self.auth_username
712 )
713 except Exception:
714 result = AUTH_FAILED
715 self._send_auth_result(username, method, result)
716 raise
717 result = AUTH_SUCCESSFUL
718 self.transport.server_object.check_auth_gssapi_keyex(
719 username, result
720 )
721 else:
722 result = self.transport.server_object.check_auth_none(username)
723 # okay, send result
724 self._send_auth_result(username, method, result)
726 def _parse_userauth_success(self, m):
727 self._log(
728 INFO, "Authentication ({}) successful!".format(self.auth_method)
729 )
730 self.authenticated = True
731 self.transport._auth_trigger()
732 if self.auth_event is not None:
733 self.auth_event.set()
735 def _parse_userauth_failure(self, m):
736 authlist = m.get_list()
737 # TODO 4.0: we aren't giving callers access to authlist _unless_ it's
738 # partial authentication, so eg authtype=none can't work unless we
739 # tweak this.
740 partial = m.get_boolean()
741 if partial:
742 self._log(INFO, "Authentication continues...")
743 self._log(DEBUG, "Methods: " + str(authlist))
744 self.transport.saved_exception = PartialAuthentication(authlist)
745 elif self.auth_method not in authlist:
746 for msg in (
747 "Authentication type ({}) not permitted.".format(
748 self.auth_method
749 ),
750 "Allowed methods: {}".format(authlist),
751 ):
752 self._log(DEBUG, msg)
753 self.transport.saved_exception = BadAuthenticationType(
754 "Bad authentication type", authlist
755 )
756 else:
757 self._log(
758 INFO, "Authentication ({}) failed.".format(self.auth_method)
759 )
760 self.authenticated = False
761 self.username = None
762 if self.auth_event is not None:
763 self.auth_event.set()
765 def _parse_userauth_banner(self, m):
766 banner = m.get_string()
767 self.banner = banner
768 self._log(INFO, "Auth banner: {}".format(banner))
769 # who cares.
771 def _parse_userauth_info_request(self, m):
772 if self.auth_method != "keyboard-interactive":
773 raise SSHException("Illegal info request from server")
774 title = m.get_text()
775 instructions = m.get_text()
776 m.get_binary() # lang
777 prompts = m.get_int()
778 prompt_list = []
779 for i in range(prompts):
780 prompt_list.append((m.get_text(), m.get_boolean()))
781 response_list = self.interactive_handler(
782 title, instructions, prompt_list
783 )
785 m = Message()
786 m.add_byte(cMSG_USERAUTH_INFO_RESPONSE)
787 m.add_int(len(response_list))
788 for r in response_list:
789 m.add_string(r)
790 self.transport._send_message(m)
792 def _parse_userauth_info_response(self, m):
793 if not self.transport.server_mode:
794 raise SSHException("Illegal info response from server")
795 n = m.get_int()
796 responses = []
797 for i in range(n):
798 responses.append(m.get_text())
799 result = self.transport.server_object.check_auth_interactive_response(
800 responses
801 )
802 if isinstance(result, InteractiveQuery):
803 # make interactive query instead of response
804 self._interactive_query(result)
805 return
806 self._send_auth_result(
807 self.auth_username, "keyboard-interactive", result
808 )
810 def _handle_local_gss_failure(self, e):
811 self.transport.saved_exception = e
812 self._log(DEBUG, "GSSAPI failure: {}".format(e))
813 self._log(INFO, "Authentication ({}) failed.".format(self.auth_method))
814 self.authenticated = False
815 self.username = None
816 if self.auth_event is not None:
817 self.auth_event.set()
818 return
820 # TODO 4.0: MAY make sense to make these tables into actual
821 # classes/instances that can be fed a mode bool or whatever. Or,
822 # alternately (both?) make the message types small classes or enums that
823 # embed this info within themselves (which could also then tidy up the
824 # current 'integer -> human readable short string' stuff in common.py).
825 # TODO: if we do that, also expose 'em publicly.
827 # Messages which should be handled _by_ servers (sent by clients)
828 @property
829 def _server_handler_table(self):
830 return {
831 # TODO 4.0: MSG_SERVICE_REQUEST ought to eventually move into
832 # Transport's server mode like the client side did, just for
833 # consistency.
834 MSG_SERVICE_REQUEST: self._parse_service_request,
835 MSG_USERAUTH_REQUEST: self._parse_userauth_request,
836 MSG_USERAUTH_INFO_RESPONSE: self._parse_userauth_info_response,
837 }
839 # Messages which should be handled _by_ clients (sent by servers)
840 @property
841 def _client_handler_table(self):
842 return {
843 MSG_SERVICE_ACCEPT: self._parse_service_accept,
844 MSG_USERAUTH_SUCCESS: self._parse_userauth_success,
845 MSG_USERAUTH_FAILURE: self._parse_userauth_failure,
846 MSG_USERAUTH_BANNER: self._parse_userauth_banner,
847 MSG_USERAUTH_INFO_REQUEST: self._parse_userauth_info_request,
848 }
850 # NOTE: prior to the fix for #1283, this was a static dict instead of a
851 # property. Should be backwards compatible in most/all cases.
852 @property
853 def _handler_table(self):
854 if self.transport.server_mode:
855 return self._server_handler_table
856 else:
857 return self._client_handler_table
860class GssapiWithMicAuthHandler:
861 """A specialized Auth handler for gssapi-with-mic
863 During the GSSAPI token exchange we need a modified dispatch table,
864 because the packet type numbers are not unique.
865 """
867 method = "gssapi-with-mic"
869 def __init__(self, delegate, sshgss):
870 self._delegate = delegate
871 self.sshgss = sshgss
873 def abort(self):
874 self._restore_delegate_auth_handler()
875 return self._delegate.abort()
877 @property
878 def transport(self):
879 return self._delegate.transport
881 @property
882 def _send_auth_result(self):
883 return self._delegate._send_auth_result
885 @property
886 def auth_username(self):
887 return self._delegate.auth_username
889 @property
890 def gss_host(self):
891 return self._delegate.gss_host
893 def _restore_delegate_auth_handler(self):
894 self.transport.auth_handler = self._delegate
896 def _parse_userauth_gssapi_token(self, m):
897 client_token = m.get_string()
898 # use the client token as input to establish a secure
899 # context.
900 sshgss = self.sshgss
901 try:
902 token = sshgss.ssh_accept_sec_context(
903 self.gss_host, client_token, self.auth_username
904 )
905 except Exception as e:
906 self.transport.saved_exception = e
907 result = AUTH_FAILED
908 self._restore_delegate_auth_handler()
909 self._send_auth_result(self.auth_username, self.method, result)
910 raise
911 if token is not None:
912 m = Message()
913 m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
914 m.add_string(token)
915 self.transport._expected_packet = (
916 MSG_USERAUTH_GSSAPI_TOKEN,
917 MSG_USERAUTH_GSSAPI_MIC,
918 MSG_USERAUTH_REQUEST,
919 )
920 self.transport._send_message(m)
922 def _parse_userauth_gssapi_mic(self, m):
923 mic_token = m.get_string()
924 sshgss = self.sshgss
925 username = self.auth_username
926 self._restore_delegate_auth_handler()
927 try:
928 sshgss.ssh_check_mic(
929 mic_token, self.transport.session_id, username
930 )
931 except Exception as e:
932 self.transport.saved_exception = e
933 result = AUTH_FAILED
934 self._send_auth_result(username, self.method, result)
935 raise
936 # TODO: Implement client credential saving.
937 # The OpenSSH server is able to create a TGT with the delegated
938 # client credentials, but this is not supported by GSS-API.
939 result = AUTH_SUCCESSFUL
940 self.transport.server_object.check_auth_gssapi_with_mic(
941 username, result
942 )
943 # okay, send result
944 self._send_auth_result(username, self.method, result)
946 def _parse_service_request(self, m):
947 self._restore_delegate_auth_handler()
948 return self._delegate._parse_service_request(m)
950 def _parse_userauth_request(self, m):
951 self._restore_delegate_auth_handler()
952 return self._delegate._parse_userauth_request(m)
954 __handler_table = {
955 MSG_SERVICE_REQUEST: _parse_service_request,
956 MSG_USERAUTH_REQUEST: _parse_userauth_request,
957 MSG_USERAUTH_GSSAPI_TOKEN: _parse_userauth_gssapi_token,
958 MSG_USERAUTH_GSSAPI_MIC: _parse_userauth_gssapi_mic,
959 }
961 @property
962 def _handler_table(self):
963 # TODO: determine if we can cut this up like we did for the primary
964 # AuthHandler class.
965 return self.__handler_table
968class AuthOnlyHandler(AuthHandler):
969 """
970 AuthHandler, and just auth, no service requests!
972 .. versionadded:: 3.2
973 """
975 # NOTE: this purposefully duplicates some of the parent class in order to
976 # modernize, refactor, etc. The intent is that eventually we will collapse
977 # this one onto the parent in a backwards incompatible release.
979 @property
980 def _client_handler_table(self):
981 my_table = super()._client_handler_table.copy()
982 del my_table[MSG_SERVICE_ACCEPT]
983 return my_table
985 def send_auth_request(self, username, method, finish_message=None):
986 """
987 Submit a userauth request message & wait for response.
989 Performs the transport message send call, sets self.auth_event, and
990 will lock-n-block as necessary to both send, and wait for response to,
991 the USERAUTH_REQUEST.
993 Most callers will want to supply a callback to ``finish_message``,
994 which accepts a Message ``m`` and may call mutator methods on it to add
995 more fields.
996 """
997 # Store a few things for reference in handlers, including auth failure
998 # handler (which needs to know if we were using a bad method, etc)
999 self.auth_method = method
1000 self.username = username
1001 # Generic userauth request fields
1002 m = Message()
1003 m.add_byte(cMSG_USERAUTH_REQUEST)
1004 m.add_string(username)
1005 m.add_string("ssh-connection")
1006 m.add_string(method)
1007 # Caller usually has more to say, such as injecting password, key etc
1008 finish_message(m)
1009 # TODO 4.0: seems odd to have the client handle the lock and not
1010 # Transport; that _may_ have been an artifact of allowing user
1011 # threading event injection? Regardless, we don't want to move _this_
1012 # locking into Transport._send_message now, because lots of other
1013 # untouched code also uses that method and we might end up
1014 # double-locking (?) but 4.0 would be a good time to revisit.
1015 with self.transport.lock:
1016 self.transport._send_message(m)
1017 # We have cut out the higher level event args, but self.auth_event is
1018 # still required for self.wait_for_response to function correctly (it's
1019 # the mechanism used by the auth success/failure handlers, the abort
1020 # handler, and a few other spots like in gssapi.
1021 # TODO: interestingly, wait_for_response itself doesn't actually
1022 # enforce that its event argument and self.auth_event are the same...
1023 self.auth_event = threading.Event()
1024 return self.wait_for_response(self.auth_event)
1026 def auth_none(self, username):
1027 return self.send_auth_request(username, "none")
1029 def auth_publickey(self, username, key):
1030 key_type, bits = self._get_key_type_and_bits(key)
1031 algorithm = self._finalize_pubkey_algorithm(key_type)
1032 blob = self._get_session_blob(
1033 key,
1034 "ssh-connection",
1035 username,
1036 algorithm,
1037 )
1039 def finish(m):
1040 # This field doesn't appear to be named, but is False when querying
1041 # for permission (ie knowing whether to even prompt a user for
1042 # passphrase, etc) or True when just going for it. Paramiko has
1043 # never bothered with the former type of message, apparently.
1044 m.add_boolean(True)
1045 m.add_string(algorithm)
1046 m.add_string(bits)
1047 m.add_string(key.sign_ssh_data(blob, algorithm))
1049 return self.send_auth_request(username, "publickey", finish)
1051 def auth_password(self, username, password):
1052 def finish(m):
1053 # Unnamed field that equates to "I am changing my password", which
1054 # Paramiko clientside never supported and serverside only sort of
1055 # supported.
1056 m.add_boolean(False)
1057 m.add_string(b(password))
1059 return self.send_auth_request(username, "password", finish)
1061 def auth_interactive(self, username, handler, submethods=""):
1062 """
1063 response_list = handler(title, instructions, prompt_list)
1064 """
1065 # Unlike most siblings, this auth method _does_ require other
1066 # superclass handlers (eg userauth info request) to understand
1067 # what's going on, so we still set some self attributes.
1068 self.auth_method = "keyboard_interactive"
1069 self.interactive_handler = handler
1071 def finish(m):
1072 # Empty string for deprecated language tag field, per RFC 4256:
1073 # https://www.rfc-editor.org/rfc/rfc4256#section-3.1
1074 m.add_string("")
1075 m.add_string(submethods)
1077 return self.send_auth_request(username, "keyboard-interactive", finish)
1079 # NOTE: not strictly 'auth only' related, but allows users to opt-in.
1080 def _choose_fallback_pubkey_algorithm(self, key_type, my_algos):
1081 msg = "Server did not send a server-sig-algs list; defaulting to something in our preferred algorithms list" # noqa
1082 self._log(DEBUG, msg)
1083 noncert_key_type = key_type.replace("-cert-v01@openssh.com", "")
1084 if key_type in my_algos or noncert_key_type in my_algos:
1085 actual = key_type if key_type in my_algos else noncert_key_type
1086 msg = f"Current key type, {actual!r}, is in our preferred list; using that" # noqa
1087 algo = actual
1088 else:
1089 algo = my_algos[0]
1090 msg = f"{key_type!r} not in our list - trying first list item instead, {algo!r}" # noqa
1091 self._log(DEBUG, msg)
1092 return algo