codekingpro/portable-devtools
115k
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 the6# terms of the GNU Lesser General Public License as published by the Free7# 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 ANY11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR12# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more13# details.14#15# You should have received a copy of the GNU Lesser General Public License16# along with Paramiko; if not, write to the Free Software Foundation, Inc.,17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.18 19"""20`.AuthHandler`21"""22 23import weakref24import threading25import time26import re27 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 Message66from paramiko.util import b, u67from paramiko.ssh_exception import (68 SSHException,69 AuthenticationException,70 BadAuthenticationType,71 PartialAuthentication,72)73from paramiko.server import InteractiveQuery74from paramiko.ssh_gss import GSSAuth, GSS_EXCEPTIONS75 76 77class AuthHandler:78 """79 Internal class to handle the mechanics of authentication.80 """81 82 def __init__(self, transport):83 self.transport = weakref.proxy(transport)84 self.username = None85 self.authenticated = False86 self.auth_event = None87 self.auth_method = ""88 self.banner = None89 self.password = None90 self.private_key = None91 self.interactive_handler = None92 self.submethods = None93 # for server mode:94 self.auth_username = None95 self.auth_fail_count = 096 # for GSSAPI97 self.gss_host = None98 self.gss_deleg_creds = True99 100 def _log(self, *args):101 return self.transport._log(*args)102 103 def is_authenticated(self):104 return self.authenticated105 106 def get_username(self):107 if self.transport.server_mode:108 return self.auth_username109 else:110 return self.username111 112 def auth_none(self, username, event):113 self.transport.lock.acquire()114 try:115 self.auth_event = event116 self.auth_method = "none"117 self.username = username118 self._request_auth()119 finally:120 self.transport.lock.release()121 122 def auth_publickey(self, username, key, event):123 self.transport.lock.acquire()124 try:125 self.auth_event = event126 self.auth_method = "publickey"127 self.username = username128 self.private_key = key129 self._request_auth()130 finally:131 self.transport.lock.release()132 133 def auth_password(self, username, password, event):134 self.transport.lock.acquire()135 try:136 self.auth_event = event137 self.auth_method = "password"138 self.username = username139 self.password = password140 self._request_auth()141 finally:142 self.transport.lock.release()143 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 = event151 self.auth_method = "keyboard-interactive"152 self.username = username153 self.interactive_handler = handler154 self.submethods = submethods155 self._request_auth()156 finally:157 self.transport.lock.release()158 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 = event163 self.auth_method = "gssapi-with-mic"164 self.username = username165 self.gss_host = gss_host166 self.gss_deleg_creds = gss_deleg_creds167 self._request_auth()168 finally:169 self.transport.lock.release()170 171 def auth_gssapi_keyex(self, username, event):172 self.transport.lock.acquire()173 try:174 self.auth_event = event175 self.auth_method = "gssapi-keyex"176 self.username = username177 self._request_auth()178 finally:179 self.transport.lock.release()180 181 def abort(self):182 if self.auth_event is not None:183 self.auth_event.set()184 185 # ...internals...186 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)192 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()201 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()210 211 def _get_key_type_and_bits(self, key):212 """213 Given any key, return its type/algorithm & bits-to-sign.214 215 Intended for input to or verification of, key signatures.216 """217 # Use certificate contents, if available, plain pubkey otherwise218 if key.public_blob:219 return key.public_blob.key_type, key.public_blob.key_blob220 else:221 return key.get_name(), key222 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()235 236 def wait_for_response(self, event):237 max_ts = None238 if self.transport.auth_timeout is not None:239 max_ts = time.time() + self.transport.auth_timeout240 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 e249 if event.is_set():250 break251 if max_ts is not None and max_ts <= time.time():252 raise AuthenticationException("Authentication timeout.")253 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 from259 # object, so type(e) won't work. :(260 # TODO 4.0: lol. just lmao.261 if issubclass(e.__class__, PartialAuthentication):262 return e.allowed_types263 raise e264 return []265 266 def _parse_service_request(self, m):267 service = m.get_text()268 if self.transport.server_mode and (service == "ssh-userauth"):269 # accepted270 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 return282 # dunno this one283 self._disconnect_service_not_available()284 285 def _generate_key_from_request(self, algorithm, keyblob):286 # For use in server mode.287 options = self.transport.preferred_pubkeys288 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 None294 return self.transport._key_info[algorithm](Message(keyblob))295 296 def _choose_fallback_pubkey_algorithm(self, key_type, my_algos):297 # Fallback: first one in our (possibly tweaked by caller) list298 pubkey_algo = my_algos[0]299 msg = "Server did not send a server-sig-algs list; defaulting to our first preferred algo ({!r})" # noqa300 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!", # noqa304 )305 return pubkey_algo306 307 def _finalize_pubkey_algorithm(self, key_type):308 # Short-circuit for non-RSA keys309 if "rsa" not in key_type:310 return key_type311 self._log(312 DEBUG,313 "Finalizing pubkey algorithm for key of type {!r}".format(314 key_type315 ),316 )317 # NOTE re #2017: When the key is an RSA cert and the remote server is318 # 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 algos320 # for certs specifically, and in tandem with various server bugs321 # regarding server-sig-algs, it's impossible to fit this into the rest322 # 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_version325 ):326 pubkey_algo = "ssh-rsa-cert-v01@openssh.com"327 self.transport._agreed_pubkey_algorithm = pubkey_algo328 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_algo333 # 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!" # noqa341 )342 # Check for server-sig-algs if supported & sent343 server_algo_str = u(344 self.transport.server_extensions.get("server-sig-algs", b(""))345 )346 pubkey_algo = None347 # Prefer to match against server-sig-algs348 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 own354 # preference order. (NOTE: purposefully using same style as in355 # 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's366 # technically for initial key exchange, not pubkey auth.367 err = "Unable to agree on a pubkey algorithm for signing a {!r} key!" # noqa368 raise AuthenticationException(err.format(key_type))369 # Fallback to something based purely on the key & our configuration370 else:371 pubkey_algo = self._choose_fallback_pubkey_algorithm(372 key_type, my_algos373 )374 if key_type.endswith("-cert-v01@openssh.com"):375 pubkey_algo += "-cert-v01@openssh.com"376 self.transport._agreed_pubkey_algorithm = pubkey_algo377 return pubkey_algo378 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 server413 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 just420 # the Kerberos V5 OID, so the server can only respond with421 # 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.username429 )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 any448 # token. If it does, we keep sending the token to449 # the server until no more token is returned.450 if next_token is None:451 break452 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 server464 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-API467 # error messages.468 # See RFC 4462 Section 3.8 in469 # http://www.ietf.org/rfc/rfc4462.txt470 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 - discarded476 raise SSHException(477 """GSS-API Error:478Major Status: {}479Minor Status: {}480Error Message: {}481""".format(482 maj_status, min_status, err_msg483 )484 )485 elif ptype == MSG_USERAUTH_FAILURE:486 self._parse_userauth_failure(m)487 return488 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_used495 ):496 kexgss = self.transport.kexgss_ctxt497 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 pass502 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 )511 512 def _send_auth_result(self, username, method, result):513 # okay, send result514 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 = True519 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 += 1530 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()535 536 def _interactive_query(self, q):537 # make interactive query instead of response538 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)548 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 return558 if self.authenticated:559 # ignore560 return561 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, username568 ),569 )570 if service != "ssh-connection":571 self._disconnect_service_not_available()572 return573 if (self.auth_username is not None) and (574 self.auth_username != username575 ):576 self._log(577 WARNING,578 "Auth rejected because the client attempted to change username in mid-flight", # noqa579 )580 self._disconnect_no_more_auth()581 return582 self.auth_username = username583 # check if GSS-API authentication is enabled584 gss_auth = self.transport.server_object.enable_auth_gssapi()585 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 pass597 if changereq:598 # always treated as failure, since we don't support changing599 # passwords, but collect the list of valid auth types from600 # the callback anyway601 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 pass607 result = AUTH_FAILED608 else:609 result = self.transport.server_object.check_auth_password(610 username, password611 )612 elif method == "publickey":613 sig_attached = m.get_boolean()614 # NOTE: server never wants to guess a client's algo, they're615 # telling us directly. No need for _finalize_pubkey_algorithm616 # 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 = None624 except Exception as e:625 msg = "Auth rejected: unsupported or mangled public key ({}: {})" # noqa626 self._log(INFO, msg.format(e.__class__.__name__, e))627 key = None628 if key is None:629 self._disconnect_no_more_auth()630 return631 # first check if this key is okay... if not, we can skip the verify632 result = self.transport.server_object.check_auth_publickey(633 username, key634 )635 if result != AUTH_FAILED:636 # key is okay, verify it637 if not sig_attached:638 # client wants to know if this key is acceptable, before it639 # signs anything... send special "ok" message640 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 return646 sig = Message(m.get_binary())647 blob = self._get_session_blob(648 key, service, username, algorithm649 )650 if not key.verify_ssh_sig(blob, sig):651 self._log(INFO, "Auth rejected: invalid signature")652 result = AUTH_FAILED653 elif method == "keyboard-interactive":654 submethods = m.get_string()655 result = self.transport.server_object.check_auth_interactive(656 username, submethods657 )658 if isinstance(result, InteractiveQuery):659 # make interactive query instead of response660 self._interactive_query(result)661 return662 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's666 # the only OID we support.667 mechs = m.get_int()668 # We can't accept more than one OID, so if the SSH client sends669 # 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 client686 supported_mech = sshgss.ssh_gss_oids("server")687 # RFC 4462 says we are not required to implement GSS-API error688 # messages. See section 3.8 in http://www.ietf.org/rfc/rfc4462.txt689 m = Message()690 m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE)691 m.add_bytes(supported_mech)692 self.transport.auth_handler = GssapiWithMicAuthHandler(693 self, sshgss694 )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 return702 elif method == "gssapi-keyex" and gss_auth:703 mic_token = m.get_string()704 sshgss = self.transport.kexgss_ctxt705 if sshgss is None:706 # If there is no valid context, we reject the authentication707 result = AUTH_FAILED708 self._send_auth_result(username, method, result)709 try:710 sshgss.ssh_check_mic(711 mic_token, self.transport.session_id, self.auth_username712 )713 except Exception:714 result = AUTH_FAILED715 self._send_auth_result(username, method, result)716 raise717 result = AUTH_SUCCESSFUL718 self.transport.server_object.check_auth_gssapi_keyex(719 username, result720 )721 else:722 result = self.transport.server_object.check_auth_none(username)723 # okay, send result724 self._send_auth_result(username, method, result)725 726 def _parse_userauth_success(self, m):727 self._log(728 INFO, "Authentication ({}) successful!".format(self.auth_method)729 )730 self.authenticated = True731 self.transport._auth_trigger()732 if self.auth_event is not None:733 self.auth_event.set()734 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's738 # partial authentication, so eg authtype=none can't work unless we739 # 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_method749 ),750 "Allowed methods: {}".format(authlist),751 ):752 self._log(DEBUG, msg)753 self.transport.saved_exception = BadAuthenticationType(754 "Bad authentication type", authlist755 )756 else:757 self._log(758 INFO, "Authentication ({}) failed.".format(self.auth_method)759 )760 self.authenticated = False761 self.username = None762 if self.auth_event is not None:763 self.auth_event.set()764 765 def _parse_userauth_banner(self, m):766 banner = m.get_string()767 self.banner = banner768 self._log(INFO, "Auth banner: {}".format(banner))769 # who cares.770 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() # lang777 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_list783 )784 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)791 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 responses801 )802 if isinstance(result, InteractiveQuery):803 # make interactive query instead of response804 self._interactive_query(result)805 return806 self._send_auth_result(807 self.auth_username, "keyboard-interactive", result808 )809 810 def _handle_local_gss_failure(self, e):811 self.transport.saved_exception = e812 self._log(DEBUG, "GSSAPI failure: {}".format(e))813 self._log(INFO, "Authentication ({}) failed.".format(self.auth_method))814 self.authenticated = False815 self.username = None816 if self.auth_event is not None:817 self.auth_event.set()818 return819 820 # TODO 4.0: MAY make sense to make these tables into actual821 # classes/instances that can be fed a mode bool or whatever. Or,822 # alternately (both?) make the message types small classes or enums that823 # embed this info within themselves (which could also then tidy up the824 # current 'integer -> human readable short string' stuff in common.py).825 # TODO: if we do that, also expose 'em publicly.826 827 # Messages which should be handled _by_ servers (sent by clients)828 @property829 def _server_handler_table(self):830 return {831 # TODO 4.0: MSG_SERVICE_REQUEST ought to eventually move into832 # Transport's server mode like the client side did, just for833 # 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 }838 839 # Messages which should be handled _by_ clients (sent by servers)840 @property841 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 }849 850 # NOTE: prior to the fix for #1283, this was a static dict instead of a851 # property. Should be backwards compatible in most/all cases.852 @property853 def _handler_table(self):854 if self.transport.server_mode:855 return self._server_handler_table856 else:857 return self._client_handler_table858 859 860class GssapiWithMicAuthHandler:861 """A specialized Auth handler for gssapi-with-mic862 863 During the GSSAPI token exchange we need a modified dispatch table,864 because the packet type numbers are not unique.865 """866 867 method = "gssapi-with-mic"868 869 def __init__(self, delegate, sshgss):870 self._delegate = delegate871 self.sshgss = sshgss872 873 def abort(self):874 self._restore_delegate_auth_handler()875 return self._delegate.abort()876 877 @property878 def transport(self):879 return self._delegate.transport880 881 @property882 def _send_auth_result(self):883 return self._delegate._send_auth_result884 885 @property886 def auth_username(self):887 return self._delegate.auth_username888 889 @property890 def gss_host(self):891 return self._delegate.gss_host892 893 def _restore_delegate_auth_handler(self):894 self.transport.auth_handler = self._delegate895 896 def _parse_userauth_gssapi_token(self, m):897 client_token = m.get_string()898 # use the client token as input to establish a secure899 # context.900 sshgss = self.sshgss901 try:902 token = sshgss.ssh_accept_sec_context(903 self.gss_host, client_token, self.auth_username904 )905 except Exception as e:906 self.transport.saved_exception = e907 result = AUTH_FAILED908 self._restore_delegate_auth_handler()909 self._send_auth_result(self.auth_username, self.method, result)910 raise911 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)921 922 def _parse_userauth_gssapi_mic(self, m):923 mic_token = m.get_string()924 sshgss = self.sshgss925 username = self.auth_username926 self._restore_delegate_auth_handler()927 try:928 sshgss.ssh_check_mic(929 mic_token, self.transport.session_id, username930 )931 except Exception as e:932 self.transport.saved_exception = e933 result = AUTH_FAILED934 self._send_auth_result(username, self.method, result)935 raise936 # TODO: Implement client credential saving.937 # The OpenSSH server is able to create a TGT with the delegated938 # client credentials, but this is not supported by GSS-API.939 result = AUTH_SUCCESSFUL940 self.transport.server_object.check_auth_gssapi_with_mic(941 username, result942 )943 # okay, send result944 self._send_auth_result(username, self.method, result)945 946 def _parse_service_request(self, m):947 self._restore_delegate_auth_handler()948 return self._delegate._parse_service_request(m)949 950 def _parse_userauth_request(self, m):951 self._restore_delegate_auth_handler()952 return self._delegate._parse_userauth_request(m)953 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 }960 961 @property962 def _handler_table(self):963 # TODO: determine if we can cut this up like we did for the primary964 # AuthHandler class.965 return self.__handler_table966 967 968class AuthOnlyHandler(AuthHandler):969 """970 AuthHandler, and just auth, no service requests!971 972 .. versionadded:: 3.2973 """974 975 # NOTE: this purposefully duplicates some of the parent class in order to976 # modernize, refactor, etc. The intent is that eventually we will collapse977 # this one onto the parent in a backwards incompatible release.978 979 @property980 def _client_handler_table(self):981 my_table = super()._client_handler_table.copy()982 del my_table[MSG_SERVICE_ACCEPT]983 return my_table984 985 def send_auth_request(self, username, method, finish_message=None):986 """987 Submit a userauth request message & wait for response.988 989 Performs the transport message send call, sets self.auth_event, and990 will lock-n-block as necessary to both send, and wait for response to,991 the USERAUTH_REQUEST.992 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 add995 more fields.996 """997 # Store a few things for reference in handlers, including auth failure998 # handler (which needs to know if we were using a bad method, etc)999 self.auth_method = method1000 self.username = username1001 # Generic userauth request fields1002 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 etc1008 finish_message(m)1009 # TODO 4.0: seems odd to have the client handle the lock and not1010 # Transport; that _may_ have been an artifact of allowing user1011 # threading event injection? Regardless, we don't want to move _this_1012 # locking into Transport._send_message now, because lots of other1013 # untouched code also uses that method and we might end up1014 # 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 is1018 # still required for self.wait_for_response to function correctly (it's1019 # the mechanism used by the auth success/failure handlers, the abort1020 # handler, and a few other spots like in gssapi.1021 # TODO: interestingly, wait_for_response itself doesn't actually1022 # 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)1025 1026 def auth_none(self, username):1027 return self.send_auth_request(username, "none")1028 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 )1038 1039 def finish(m):1040 # This field doesn't appear to be named, but is False when querying1041 # for permission (ie knowing whether to even prompt a user for1042 # passphrase, etc) or True when just going for it. Paramiko has1043 # 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))1048 1049 return self.send_auth_request(username, "publickey", finish)1050 1051 def auth_password(self, username, password):1052 def finish(m):1053 # Unnamed field that equates to "I am changing my password", which1054 # Paramiko clientside never supported and serverside only sort of1055 # supported.1056 m.add_boolean(False)1057 m.add_string(b(password))1058 1059 return self.send_auth_request(username, "password", finish)1060 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 other1066 # superclass handlers (eg userauth info request) to understand1067 # what's going on, so we still set some self attributes.1068 self.auth_method = "keyboard_interactive"1069 self.interactive_handler = handler1070 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.11074 m.add_string("")1075 m.add_string(submethods)1076 1077 return self.send_auth_request(username, "keyboard-interactive", finish)1078 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" # noqa1082 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_type1086 msg = f"Current key type, {actual!r}, is in our preferred list; using that" # noqa1087 algo = actual1088 else:1089 algo = my_algos[0]1090 msg = f"{key_type!r} not in our list - trying first list item instead, {algo!r}" # noqa1091 self._log(DEBUG, msg)1092 return algo1093 