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

629 statements  

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. 

18 

19""" 

20`.AuthHandler` 

21""" 

22 

23import weakref 

24import threading 

25import time 

26import re 

27 

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 

75 

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 = 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 

99 

100 def _log(self, *args): 

101 return self.transport._log(*args) 

102 

103 def is_authenticated(self): 

104 return self.authenticated 

105 

106 def get_username(self): 

107 if self.transport.server_mode: 

108 return self.auth_username 

109 else: 

110 return self.username 

111 

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() 

121 

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() 

132 

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() 

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 = 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() 

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 = 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() 

170 

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() 

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 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 

222 

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 = 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.") 

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 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 [] 

265 

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() 

284 

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)) 

295 

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 

306 

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 

378 

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 ) 

511 

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() 

535 

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) 

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 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() 

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 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) 

725 

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() 

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'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() 

764 

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. 

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() # 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 ) 

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 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 ) 

809 

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 

819 

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. 

826 

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 } 

838 

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 } 

849 

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 

858 

859 

860class GssapiWithMicAuthHandler: 

861 """A specialized Auth handler for gssapi-with-mic 

862 

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 = delegate 

871 self.sshgss = sshgss 

872 

873 def abort(self): 

874 self._restore_delegate_auth_handler() 

875 return self._delegate.abort() 

876 

877 @property 

878 def transport(self): 

879 return self._delegate.transport 

880 

881 @property 

882 def _send_auth_result(self): 

883 return self._delegate._send_auth_result 

884 

885 @property 

886 def auth_username(self): 

887 return self._delegate.auth_username 

888 

889 @property 

890 def gss_host(self): 

891 return self._delegate.gss_host 

892 

893 def _restore_delegate_auth_handler(self): 

894 self.transport.auth_handler = self._delegate 

895 

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) 

921 

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) 

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 @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 

966 

967 

968class AuthOnlyHandler(AuthHandler): 

969 """ 

970 AuthHandler, and just auth, no service requests! 

971 

972 .. versionadded:: 3.2 

973 """ 

974 

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. 

978 

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 

984 

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, and 

990 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 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) 

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 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)) 

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", which 

1054 # Paramiko clientside never supported and serverside only sort of 

1055 # 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 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 

1070 

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) 

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" # 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