ssh_gss.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. # Copyright (C) 2013-2014 science + computing ag
  2. # Author: Sebastian Deiss <sebastian.deiss@t-online.de>
  3. #
  4. #
  5. # This file is part of paramiko.
  6. #
  7. # Paramiko is free software; you can redistribute it and/or modify it under the
  8. # terms of the GNU Lesser General Public License as published by the Free
  9. # Software Foundation; either version 2.1 of the License, or (at your option)
  10. # any later version.
  11. #
  12. # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
  13. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  14. # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  15. # details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public License
  18. # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
  19. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
  20. """
  21. This module provides GSS-API / SSPI authentication as defined in :rfc:`4462`.
  22. .. note:: Credential delegation is not supported in server mode.
  23. .. seealso:: :doc:`/api/kex_gss`
  24. .. versionadded:: 1.15
  25. """
  26. import struct
  27. import os
  28. import sys
  29. """
  30. :var bool GSS_AUTH_AVAILABLE:
  31. Constraint that indicates if GSS-API / SSPI is available.
  32. """
  33. GSS_AUTH_AVAILABLE = True
  34. from pyasn1.type.univ import ObjectIdentifier
  35. from pyasn1.codec.der import encoder, decoder
  36. from paramiko.common import MSG_USERAUTH_REQUEST
  37. from paramiko.ssh_exception import SSHException
  38. """
  39. :var str _API: Constraint for the used API
  40. """
  41. _API = "MIT"
  42. try:
  43. import gssapi
  44. except (ImportError, OSError):
  45. try:
  46. import sspicon
  47. import sspi
  48. _API = "SSPI"
  49. except ImportError:
  50. GSS_AUTH_AVAILABLE = False
  51. _API = None
  52. def GSSAuth(auth_method, gss_deleg_creds=True):
  53. """
  54. Provide SSH2 GSS-API / SSPI authentication.
  55. :param str auth_method: The name of the SSH authentication mechanism
  56. (gssapi-with-mic or gss-keyex)
  57. :param bool gss_deleg_creds: Delegate client credentials or not.
  58. We delegate credentials by default.
  59. :return: Either an `._SSH_GSSAPI` (Unix) object or an
  60. `_SSH_SSPI` (Windows) object
  61. :raises: ``ImportError`` -- If no GSS-API / SSPI module could be imported.
  62. :see: `RFC 4462 <http://www.ietf.org/rfc/rfc4462.txt>`_
  63. :note: Check for the available API and return either an `._SSH_GSSAPI`
  64. (MIT GSSAPI) object or an `._SSH_SSPI` (MS SSPI) object. If you
  65. get python-gssapi working on Windows, python-gssapi
  66. will be used and a `._SSH_GSSAPI` object will be returned.
  67. If there is no supported API available,
  68. ``None`` will be returned.
  69. """
  70. if _API == "MIT":
  71. return _SSH_GSSAPI(auth_method, gss_deleg_creds)
  72. elif _API == "SSPI" and os.name == "nt":
  73. return _SSH_SSPI(auth_method, gss_deleg_creds)
  74. else:
  75. raise ImportError("Unable to import a GSS-API / SSPI module!")
  76. class _SSH_GSSAuth(object):
  77. """
  78. Contains the shared variables and methods of `._SSH_GSSAPI` and
  79. `._SSH_SSPI`.
  80. """
  81. def __init__(self, auth_method, gss_deleg_creds):
  82. """
  83. :param str auth_method: The name of the SSH authentication mechanism
  84. (gssapi-with-mic or gss-keyex)
  85. :param bool gss_deleg_creds: Delegate client credentials or not
  86. """
  87. self._auth_method = auth_method
  88. self._gss_deleg_creds = gss_deleg_creds
  89. self._gss_host = None
  90. self._username = None
  91. self._session_id = None
  92. self._service = "ssh-connection"
  93. """
  94. OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication,
  95. so we also support the krb5 mechanism only.
  96. """
  97. self._krb5_mech = "1.2.840.113554.1.2.2"
  98. # client mode
  99. self._gss_ctxt = None
  100. self._gss_ctxt_status = False
  101. # server mode
  102. self._gss_srv_ctxt = None
  103. self._gss_srv_ctxt_status = False
  104. self.cc_file = None
  105. def set_service(self, service):
  106. """
  107. This is just a setter to use a non default service.
  108. I added this method, because RFC 4462 doesn't specify "ssh-connection"
  109. as the only service value.
  110. :param str service: The desired SSH service
  111. """
  112. if service.find("ssh-"):
  113. self._service = service
  114. def set_username(self, username):
  115. """
  116. Setter for C{username}. If GSS-API Key Exchange is performed, the
  117. username is not set by C{ssh_init_sec_context}.
  118. :param str username: The name of the user who attempts to login
  119. """
  120. self._username = username
  121. def ssh_gss_oids(self, mode="client"):
  122. """
  123. This method returns a single OID, because we only support the
  124. Kerberos V5 mechanism.
  125. :param str mode: Client for client mode and server for server mode
  126. :return: A byte sequence containing the number of supported
  127. OIDs, the length of the OID and the actual OID encoded with
  128. DER
  129. :note: In server mode we just return the OID length and the DER encoded
  130. OID.
  131. """
  132. OIDs = self._make_uint32(1)
  133. krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech))
  134. OID_len = self._make_uint32(len(krb5_OID))
  135. if mode == "server":
  136. return OID_len + krb5_OID
  137. return OIDs + OID_len + krb5_OID
  138. def ssh_check_mech(self, desired_mech):
  139. """
  140. Check if the given OID is the Kerberos V5 OID (server mode).
  141. :param str desired_mech: The desired GSS-API mechanism of the client
  142. :return: ``True`` if the given OID is supported, otherwise C{False}
  143. """
  144. mech, __ = decoder.decode(desired_mech)
  145. if mech.__str__() != self._krb5_mech:
  146. return False
  147. return True
  148. # Internals
  149. # -------------------------------------------------------------------------
  150. def _make_uint32(self, integer):
  151. """
  152. Create a 32 bit unsigned integer (The byte sequence of an integer).
  153. :param int integer: The integer value to convert
  154. :return: The byte sequence of an 32 bit integer
  155. """
  156. return struct.pack("!I", integer)
  157. def _ssh_build_mic(self, session_id, username, service, auth_method):
  158. """
  159. Create the SSH2 MIC filed for gssapi-with-mic.
  160. :param str session_id: The SSH session ID
  161. :param str username: The name of the user who attempts to login
  162. :param str service: The requested SSH service
  163. :param str auth_method: The requested SSH authentication mechanism
  164. :return: The MIC as defined in RFC 4462. The contents of the
  165. MIC field are:
  166. string session_identifier,
  167. byte SSH_MSG_USERAUTH_REQUEST,
  168. string user-name,
  169. string service (ssh-connection),
  170. string authentication-method
  171. (gssapi-with-mic or gssapi-keyex)
  172. """
  173. mic = self._make_uint32(len(session_id))
  174. mic += session_id
  175. mic += struct.pack('B', MSG_USERAUTH_REQUEST)
  176. mic += self._make_uint32(len(username))
  177. mic += username.encode()
  178. mic += self._make_uint32(len(service))
  179. mic += service.encode()
  180. mic += self._make_uint32(len(auth_method))
  181. mic += auth_method.encode()
  182. return mic
  183. class _SSH_GSSAPI(_SSH_GSSAuth):
  184. """
  185. Implementation of the GSS-API MIT Kerberos Authentication for SSH2.
  186. :see: `.GSSAuth`
  187. """
  188. def __init__(self, auth_method, gss_deleg_creds):
  189. """
  190. :param str auth_method: The name of the SSH authentication mechanism
  191. (gssapi-with-mic or gss-keyex)
  192. :param bool gss_deleg_creds: Delegate client credentials or not
  193. """
  194. _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds)
  195. if self._gss_deleg_creds:
  196. self._gss_flags = (gssapi.C_PROT_READY_FLAG,
  197. gssapi.C_INTEG_FLAG,
  198. gssapi.C_MUTUAL_FLAG,
  199. gssapi.C_DELEG_FLAG)
  200. else:
  201. self._gss_flags = (gssapi.C_PROT_READY_FLAG,
  202. gssapi.C_INTEG_FLAG,
  203. gssapi.C_MUTUAL_FLAG)
  204. def ssh_init_sec_context(self, target, desired_mech=None,
  205. username=None, recv_token=None):
  206. """
  207. Initialize a GSS-API context.
  208. :param str username: The name of the user who attempts to login
  209. :param str target: The hostname of the target to connect to
  210. :param str desired_mech: The negotiated GSS-API mechanism
  211. ("pseudo negotiated" mechanism, because we
  212. support just the krb5 mechanism :-))
  213. :param str recv_token: The GSS-API token received from the Server
  214. :raises:
  215. `.SSHException` -- Is raised if the desired mechanism of the client
  216. is not supported
  217. :return: A ``String`` if the GSS-API has returned a token or
  218. ``None`` if no token was returned
  219. """
  220. self._username = username
  221. self._gss_host = target
  222. targ_name = gssapi.Name("host@" + self._gss_host,
  223. gssapi.C_NT_HOSTBASED_SERVICE)
  224. ctx = gssapi.Context()
  225. ctx.flags = self._gss_flags
  226. if desired_mech is None:
  227. krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech)
  228. else:
  229. mech, __ = decoder.decode(desired_mech)
  230. if mech.__str__() != self._krb5_mech:
  231. raise SSHException("Unsupported mechanism OID.")
  232. else:
  233. krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech)
  234. token = None
  235. try:
  236. if recv_token is None:
  237. self._gss_ctxt = gssapi.InitContext(peer_name=targ_name,
  238. mech_type=krb5_mech,
  239. req_flags=ctx.flags)
  240. token = self._gss_ctxt.step(token)
  241. else:
  242. token = self._gss_ctxt.step(recv_token)
  243. except gssapi.GSSException:
  244. message = "{0} Target: {1}".format(
  245. sys.exc_info()[1], self._gss_host)
  246. raise gssapi.GSSException(message)
  247. self._gss_ctxt_status = self._gss_ctxt.established
  248. return token
  249. def ssh_get_mic(self, session_id, gss_kex=False):
  250. """
  251. Create the MIC token for a SSH2 message.
  252. :param str session_id: The SSH session ID
  253. :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not
  254. :return: gssapi-with-mic:
  255. Returns the MIC token from GSS-API for the message we created
  256. with ``_ssh_build_mic``.
  257. gssapi-keyex:
  258. Returns the MIC token from GSS-API with the SSH session ID as
  259. message.
  260. """
  261. self._session_id = session_id
  262. if not gss_kex:
  263. mic_field = self._ssh_build_mic(self._session_id,
  264. self._username,
  265. self._service,
  266. self._auth_method)
  267. mic_token = self._gss_ctxt.get_mic(mic_field)
  268. else:
  269. # for key exchange with gssapi-keyex
  270. mic_token = self._gss_srv_ctxt.get_mic(self._session_id)
  271. return mic_token
  272. def ssh_accept_sec_context(self, hostname, recv_token, username=None):
  273. """
  274. Accept a GSS-API context (server mode).
  275. :param str hostname: The servers hostname
  276. :param str username: The name of the user who attempts to login
  277. :param str recv_token: The GSS-API Token received from the server,
  278. if it's not the initial call.
  279. :return: A ``String`` if the GSS-API has returned a token or ``None``
  280. if no token was returned
  281. """
  282. # hostname and username are not required for GSSAPI, but for SSPI
  283. self._gss_host = hostname
  284. self._username = username
  285. if self._gss_srv_ctxt is None:
  286. self._gss_srv_ctxt = gssapi.AcceptContext()
  287. token = self._gss_srv_ctxt.step(recv_token)
  288. self._gss_srv_ctxt_status = self._gss_srv_ctxt.established
  289. return token
  290. def ssh_check_mic(self, mic_token, session_id, username=None):
  291. """
  292. Verify the MIC token for a SSH2 message.
  293. :param str mic_token: The MIC token received from the client
  294. :param str session_id: The SSH session ID
  295. :param str username: The name of the user who attempts to login
  296. :return: None if the MIC check was successful
  297. :raises: ``gssapi.GSSException`` -- if the MIC check failed
  298. """
  299. self._session_id = session_id
  300. self._username = username
  301. if self._username is not None:
  302. # server mode
  303. mic_field = self._ssh_build_mic(self._session_id,
  304. self._username,
  305. self._service,
  306. self._auth_method)
  307. self._gss_srv_ctxt.verify_mic(mic_field, mic_token)
  308. else:
  309. # for key exchange with gssapi-keyex
  310. # client mode
  311. self._gss_ctxt.verify_mic(self._session_id,
  312. mic_token)
  313. @property
  314. def credentials_delegated(self):
  315. """
  316. Checks if credentials are delegated (server mode).
  317. :return: ``True`` if credentials are delegated, otherwise ``False``
  318. """
  319. if self._gss_srv_ctxt.delegated_cred is not None:
  320. return True
  321. return False
  322. def save_client_creds(self, client_token):
  323. """
  324. Save the Client token in a file. This is used by the SSH server
  325. to store the client credentials if credentials are delegated
  326. (server mode).
  327. :param str client_token: The GSS-API token received form the client
  328. :raises:
  329. ``NotImplementedError`` -- Credential delegation is currently not
  330. supported in server mode
  331. """
  332. raise NotImplementedError
  333. class _SSH_SSPI(_SSH_GSSAuth):
  334. """
  335. Implementation of the Microsoft SSPI Kerberos Authentication for SSH2.
  336. :see: `.GSSAuth`
  337. """
  338. def __init__(self, auth_method, gss_deleg_creds):
  339. """
  340. :param str auth_method: The name of the SSH authentication mechanism
  341. (gssapi-with-mic or gss-keyex)
  342. :param bool gss_deleg_creds: Delegate client credentials or not
  343. """
  344. _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds)
  345. if self._gss_deleg_creds:
  346. self._gss_flags = (
  347. sspicon.ISC_REQ_INTEGRITY |
  348. sspicon.ISC_REQ_MUTUAL_AUTH |
  349. sspicon.ISC_REQ_DELEGATE
  350. )
  351. else:
  352. self._gss_flags = (
  353. sspicon.ISC_REQ_INTEGRITY |
  354. sspicon.ISC_REQ_MUTUAL_AUTH
  355. )
  356. def ssh_init_sec_context(self, target, desired_mech=None,
  357. username=None, recv_token=None):
  358. """
  359. Initialize a SSPI context.
  360. :param str username: The name of the user who attempts to login
  361. :param str target: The FQDN of the target to connect to
  362. :param str desired_mech: The negotiated SSPI mechanism
  363. ("pseudo negotiated" mechanism, because we
  364. support just the krb5 mechanism :-))
  365. :param recv_token: The SSPI token received from the Server
  366. :raises:
  367. `.SSHException` -- Is raised if the desired mechanism of the client
  368. is not supported
  369. :return: A ``String`` if the SSPI has returned a token or ``None`` if
  370. no token was returned
  371. """
  372. self._username = username
  373. self._gss_host = target
  374. error = 0
  375. targ_name = "host/" + self._gss_host
  376. if desired_mech is not None:
  377. mech, __ = decoder.decode(desired_mech)
  378. if mech.__str__() != self._krb5_mech:
  379. raise SSHException("Unsupported mechanism OID.")
  380. try:
  381. if recv_token is None:
  382. self._gss_ctxt = sspi.ClientAuth("Kerberos",
  383. scflags=self._gss_flags,
  384. targetspn=targ_name)
  385. error, token = self._gss_ctxt.authorize(recv_token)
  386. token = token[0].Buffer
  387. except:
  388. raise Exception("{0}, Target: {1}".format(sys.exc_info()[1],
  389. self._gss_host))
  390. if error == 0:
  391. """
  392. if the status is GSS_COMPLETE (error = 0) the context is fully
  393. established an we can set _gss_ctxt_status to True.
  394. """
  395. self._gss_ctxt_status = True
  396. token = None
  397. """
  398. You won't get another token if the context is fully established,
  399. so i set token to None instead of ""
  400. """
  401. return token
  402. def ssh_get_mic(self, session_id, gss_kex=False):
  403. """
  404. Create the MIC token for a SSH2 message.
  405. :param str session_id: The SSH session ID
  406. :param bool gss_kex: Generate the MIC for Key Exchange with SSPI or not
  407. :return: gssapi-with-mic:
  408. Returns the MIC token from SSPI for the message we created
  409. with ``_ssh_build_mic``.
  410. gssapi-keyex:
  411. Returns the MIC token from SSPI with the SSH session ID as
  412. message.
  413. """
  414. self._session_id = session_id
  415. if not gss_kex:
  416. mic_field = self._ssh_build_mic(self._session_id,
  417. self._username,
  418. self._service,
  419. self._auth_method)
  420. mic_token = self._gss_ctxt.sign(mic_field)
  421. else:
  422. # for key exchange with gssapi-keyex
  423. mic_token = self._gss_srv_ctxt.sign(self._session_id)
  424. return mic_token
  425. def ssh_accept_sec_context(self, hostname, username, recv_token):
  426. """
  427. Accept a SSPI context (server mode).
  428. :param str hostname: The servers FQDN
  429. :param str username: The name of the user who attempts to login
  430. :param str recv_token: The SSPI Token received from the server,
  431. if it's not the initial call.
  432. :return: A ``String`` if the SSPI has returned a token or ``None`` if
  433. no token was returned
  434. """
  435. self._gss_host = hostname
  436. self._username = username
  437. targ_name = "host/" + self._gss_host
  438. self._gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=targ_name)
  439. error, token = self._gss_srv_ctxt.authorize(recv_token)
  440. token = token[0].Buffer
  441. if error == 0:
  442. self._gss_srv_ctxt_status = True
  443. token = None
  444. return token
  445. def ssh_check_mic(self, mic_token, session_id, username=None):
  446. """
  447. Verify the MIC token for a SSH2 message.
  448. :param str mic_token: The MIC token received from the client
  449. :param str session_id: The SSH session ID
  450. :param str username: The name of the user who attempts to login
  451. :return: None if the MIC check was successful
  452. :raises: ``sspi.error`` -- if the MIC check failed
  453. """
  454. self._session_id = session_id
  455. self._username = username
  456. if username is not None:
  457. # server mode
  458. mic_field = self._ssh_build_mic(self._session_id,
  459. self._username,
  460. self._service,
  461. self._auth_method)
  462. # Verifies data and its signature. If verification fails, an
  463. # sspi.error will be raised.
  464. self._gss_srv_ctxt.verify(mic_field, mic_token)
  465. else:
  466. # for key exchange with gssapi-keyex
  467. # client mode
  468. # Verifies data and its signature. If verification fails, an
  469. # sspi.error will be raised.
  470. self._gss_ctxt.verify(self._session_id, mic_token)
  471. @property
  472. def credentials_delegated(self):
  473. """
  474. Checks if credentials are delegated (server mode).
  475. :return: ``True`` if credentials are delegated, otherwise ``False``
  476. """
  477. return (
  478. self._gss_flags & sspicon.ISC_REQ_DELEGATE and
  479. (self._gss_srv_ctxt_status or self._gss_flags)
  480. )
  481. def save_client_creds(self, client_token):
  482. """
  483. Save the Client token in a file. This is used by the SSH server
  484. to store the client credentails if credentials are delegated
  485. (server mode).
  486. :param str client_token: The SSPI token received form the client
  487. :raises:
  488. ``NotImplementedError`` -- Credential delegation is currently not
  489. supported in server mode
  490. """
  491. raise NotImplementedError