# Copyright (C) 2013-2014 science + computing ag # Author: Sebastian Deiss # # # This file is part of paramiko. # # Paramiko is free software; you can redistribute it and/or modify it under the # terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ This module provides GSS-API / SSPI authentication as defined in :rfc:`4462`. .. note:: Credential delegation is not supported in server mode. .. seealso:: :doc:`/api/kex_gss` .. versionadded:: 1.15 """ import struct import os import sys #: A boolean constraint that indicates if GSS-API / SSPI is available. GSS_AUTH_AVAILABLE = True #: A tuple of the exception types used by the underlying GSSAPI implementation. GSS_EXCEPTIONS = () #: :var str _API: Constraint for the used API _API = None try: import gssapi if hasattr(gssapi, "__title__") and gssapi.__title__ == "python-gssapi": # old, unmaintained python-gssapi package _API = "MIT" # keep this for compatibility GSS_EXCEPTIONS = (gssapi.GSSException,) else: _API = "PYTHON-GSSAPI-NEW" GSS_EXCEPTIONS = ( gssapi.exceptions.GeneralError, gssapi.raw.misc.GSSError, ) except (ImportError, OSError): try: import pywintypes import sspicon import sspi _API = "SSPI" GSS_EXCEPTIONS = (pywintypes.error,) except ImportError: GSS_AUTH_AVAILABLE = False _API = None from paramiko.common import MSG_USERAUTH_REQUEST from paramiko.ssh_exception import SSHException from paramiko._version import __version_info__ def GSSAuth(auth_method, gss_deleg_creds=True): """ Provide SSH2 GSS-API / SSPI authentication. :param str auth_method: The name of the SSH authentication mechanism (gssapi-with-mic or gss-keyex) :param bool gss_deleg_creds: Delegate client credentials or not. We delegate credentials by default. :return: Either an `._SSH_GSSAPI_OLD` or `._SSH_GSSAPI_NEW` (Unix) object or an `_SSH_SSPI` (Windows) object :rtype: object :raises: ``ImportError`` -- If no GSS-API / SSPI module could be imported. :see: `RFC 4462 `_ :note: Check for the available API and return either an `._SSH_GSSAPI_OLD` (MIT GSSAPI using python-gssapi package) object, an `._SSH_GSSAPI_NEW` (MIT GSSAPI using gssapi package) object or an `._SSH_SSPI` (MS SSPI) object. If there is no supported API available, ``None`` will be returned. """ if _API == "MIT": return _SSH_GSSAPI_OLD(auth_method, gss_deleg_creds) elif _API == "PYTHON-GSSAPI-NEW": return _SSH_GSSAPI_NEW(auth_method, gss_deleg_creds) elif _API == "SSPI" and os.name == "nt": return _SSH_SSPI(auth_method, gss_deleg_creds) else: raise ImportError("Unable to import a GSS-API / SSPI module!") class _SSH_GSSAuth: """ Contains the shared variables and methods of `._SSH_GSSAPI_OLD`, `._SSH_GSSAPI_NEW` and `._SSH_SSPI`. """ def __init__(self, auth_method, gss_deleg_creds): """ :param str auth_method: The name of the SSH authentication mechanism (gssapi-with-mic or gss-keyex) :param bool gss_deleg_creds: Delegate client credentials or not """ self._auth_method = auth_method self._gss_deleg_creds = gss_deleg_creds self._gss_host = None self._username = None self._session_id = None self._service = "ssh-connection" """ OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication, so we also support the krb5 mechanism only. """ self._krb5_mech = "1.2.840.113554.1.2.2" # client mode self._gss_ctxt = None self._gss_ctxt_status = False # server mode self._gss_srv_ctxt = None self._gss_srv_ctxt_status = False self.cc_file = None def set_service(self, service): """ This is just a setter to use a non default service. I added this method, because RFC 4462 doesn't specify "ssh-connection" as the only service value. :param str service: The desired SSH service """ if service.find("ssh-"): self._service = service def set_username(self, username): """ Setter for C{username}. If GSS-API Key Exchange is performed, the username is not set by C{ssh_init_sec_context}. :param str username: The name of the user who attempts to login """ self._username = username def ssh_gss_oids(self, mode="client"): """ This method returns a single OID, because we only support the Kerberos V5 mechanism. :param str mode: Client for client mode and server for server mode :return: A byte sequence containing the number of supported OIDs, the length of the OID and the actual OID encoded with DER :note: In server mode we just return the OID length and the DER encoded OID. """ from pyasn1.type.univ import ObjectIdentifier from pyasn1.codec.der import encoder OIDs = self._make_uint32(1) krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech)) OID_len = self._make_uint32(len(krb5_OID)) if mode == "server": return OID_len + krb5_OID return OIDs + OID_len + krb5_OID def ssh_check_mech(self, desired_mech): """ Check if the given OID is the Kerberos V5 OID (server mode). :param str desired_mech: The desired GSS-API mechanism of the client :return: ``True`` if the given OID is supported, otherwise C{False} """ from pyasn1.codec.der import decoder mech, __ = decoder.decode(desired_mech) if mech.__str__() != self._krb5_mech: return False return True # Internals # ------------------------------------------------------------------------- def _make_uint32(self, integer): """ Create a 32 bit unsigned integer (The byte sequence of an integer). :param int integer: The integer value to convert :return: The byte sequence of an 32 bit integer """ return struct.pack("!I", integer) def _ssh_build_mic(self, session_id, username, service, auth_method): """ Create the SSH2 MIC filed for gssapi-with-mic. :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login :param str service: The requested SSH service :param str auth_method: The requested SSH authentication mechanism :return: The MIC as defined in RFC 4462. The contents of the MIC field are: string session_identifier, byte SSH_MSG_USERAUTH_REQUEST, string user-name, string service (ssh-connection), string authentication-method (gssapi-with-mic or gssapi-keyex) """ mic = self._make_uint32(len(session_id)) mic += session_id mic += struct.pack("B", MSG_USERAUTH_REQUEST) mic += self._make_uint32(len(username)) mic += username.encode() mic += self._make_uint32(len(service)) mic += service.encode() mic += self._make_uint32(len(auth_method)) mic += auth_method.encode() return mic class _SSH_GSSAPI_OLD(_SSH_GSSAuth): """ Implementation of the GSS-API MIT Kerberos Authentication for SSH2, using the older (unmaintained) python-gssapi package. :see: `.GSSAuth` """ def __init__(self, auth_method, gss_deleg_creds): """ :param str auth_method: The name of the SSH authentication mechanism (gssapi-with-mic or gss-keyex) :param bool gss_deleg_creds: Delegate client credentials or not """ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) if self._gss_deleg_creds: self._gss_flags = ( gssapi.C_PROT_READY_FLAG, gssapi.C_INTEG_FLAG, gssapi.C_MUTUAL_FLAG, gssapi.C_DELEG_FLAG, ) else: self._gss_flags = ( gssapi.C_PROT_READY_FLAG, gssapi.C_INTEG_FLAG, gssapi.C_MUTUAL_FLAG, ) def ssh_init_sec_context( self, target, desired_mech=None, username=None, recv_token=None ): """ Initialize a GSS-API context. :param str username: The name of the user who attempts to login :param str target: The hostname of the target to connect to :param str desired_mech: The negotiated GSS-API mechanism ("pseudo negotiated" mechanism, because we support just the krb5 mechanism :-)) :param str recv_token: The GSS-API token received from the Server :raises: `.SSHException` -- Is raised if the desired mechanism of the client is not supported :return: A ``String`` if the GSS-API has returned a token or ``None`` if no token was returned """ from pyasn1.codec.der import decoder self._username = username self._gss_host = target targ_name = gssapi.Name( "host@" + self._gss_host, gssapi.C_NT_HOSTBASED_SERVICE ) ctx = gssapi.Context() ctx.flags = self._gss_flags if desired_mech is None: krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) else: mech, __ = decoder.decode(desired_mech) if mech.__str__() != self._krb5_mech: raise SSHException("Unsupported mechanism OID.") else: krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) token = None try: if recv_token is None: self._gss_ctxt = gssapi.InitContext( peer_name=targ_name, mech_type=krb5_mech, req_flags=ctx.flags, ) token = self._gss_ctxt.step(token) else: token = self._gss_ctxt.step(recv_token) except gssapi.GSSException: message = "{} Target: {}".format(sys.exc_info()[1], self._gss_host) raise gssapi.GSSException(message) self._gss_ctxt_status = self._gss_ctxt.established return token def ssh_get_mic(self, session_id, gss_kex=False): """ Create the MIC token for a SSH2 message. :param str session_id: The SSH session ID :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not :return: gssapi-with-mic: Returns the MIC token from GSS-API for the message we created with ``_ssh_build_mic``. gssapi-keyex: Returns the MIC token from GSS-API with the SSH session ID as message. """ self._session_id = session_id if not gss_kex: mic_field = self._ssh_build_mic( self._session_id, self._username, self._service, self._auth_method, ) mic_token = self._gss_ctxt.get_mic(mic_field) else: # for key exchange with gssapi-keyex mic_token = self._gss_srv_ctxt.get_mic(self._session_id) return mic_token def ssh_accept_sec_context(self, hostname, recv_token, username=None): """ Accept a GSS-API context (server mode). :param str hostname: The servers hostname :param str username: The name of the user who attempts to login :param str recv_token: The GSS-API Token received from the server, if it's not the initial call. :return: A ``String`` if the GSS-API has returned a token or ``None`` if no token was returned """ # hostname and username are not required for GSSAPI, but for SSPI self._gss_host = hostname self._username = username if self._gss_srv_ctxt is None: self._gss_srv_ctxt = gssapi.AcceptContext() token = self._gss_srv_ctxt.step(recv_token) self._gss_srv_ctxt_status = self._gss_srv_ctxt.established return token def ssh_check_mic(self, mic_token, session_id, username=None): """ Verify the MIC token for a SSH2 message. :param str mic_token: The MIC token received from the client :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login :return: None if the MIC check was successful :raises: ``gssapi.GSSException`` -- if the MIC check failed """ self._session_id = session_id self._username = username if self._username is not None: # server mode mic_field = self._ssh_build_mic( self._session_id, self._username, self._service, self._auth_method, ) self._gss_srv_ctxt.verify_mic(mic_field, mic_token) else: # for key exchange with gssapi-keyex # client mode self._gss_ctxt.verify_mic(self._session_id, mic_token) @property def credentials_delegated(self): """ Checks if credentials are delegated (server mode). :return: ``True`` if credentials are delegated, otherwise ``False`` """ if self._gss_srv_ctxt.delegated_cred is not None: return True return False def save_client_creds(self, client_token): """ Save the Client token in a file. This is used by the SSH server to store the client credentials if credentials are delegated (server mode). :param str client_token: The GSS-API token received form the client :raises: ``NotImplementedError`` -- Credential delegation is currently not supported in server mode """ raise NotImplementedError if __version_info__ < (2, 5): # provide the old name for strict backward compatibility _SSH_GSSAPI = _SSH_GSSAPI_OLD class _SSH_GSSAPI_NEW(_SSH_GSSAuth): """ Implementation of the GSS-API MIT Kerberos Authentication for SSH2, using the newer, currently maintained gssapi package. :see: `.GSSAuth` """ def __init__(self, auth_method, gss_deleg_creds): """ :param str auth_method: The name of the SSH authentication mechanism (gssapi-with-mic or gss-keyex) :param bool gss_deleg_creds: Delegate client credentials or not """ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) if self._gss_deleg_creds: self._gss_flags = ( gssapi.RequirementFlag.protection_ready, gssapi.RequirementFlag.integrity, gssapi.RequirementFlag.mutual_authentication, gssapi.RequirementFlag.delegate_to_peer, ) else: self._gss_flags = ( gssapi.RequirementFlag.protection_ready, gssapi.RequirementFlag.integrity, gssapi.RequirementFlag.mutual_authentication, ) def ssh_init_sec_context( self, target, desired_mech=None, username=None, recv_token=None ): """ Initialize a GSS-API context. :param str username: The name of the user who attempts to login :param str target: The hostname of the target to connect to :param str desired_mech: The negotiated GSS-API mechanism ("pseudo negotiated" mechanism, because we support just the krb5 mechanism :-)) :param str recv_token: The GSS-API token received from the Server :raises: `.SSHException` -- Is raised if the desired mechanism of the client is not supported :raises: ``gssapi.exceptions.GSSError`` if there is an error signaled by the GSS-API implementation :return: A ``String`` if the GSS-API has returned a token or ``None`` if no token was returned """ from pyasn1.codec.der import decoder self._username = username self._gss_host = target targ_name = gssapi.Name( "host@" + self._gss_host, name_type=gssapi.NameType.hostbased_service, ) if desired_mech is not None: mech, __ = decoder.decode(desired_mech) if mech.__str__() != self._krb5_mech: raise SSHException("Unsupported mechanism OID.") krb5_mech = gssapi.MechType.kerberos token = None if recv_token is None: self._gss_ctxt = gssapi.SecurityContext( name=targ_name, flags=self._gss_flags, mech=krb5_mech, usage="initiate", ) token = self._gss_ctxt.step(token) else: token = self._gss_ctxt.step(recv_token) self._gss_ctxt_status = self._gss_ctxt.complete return token def ssh_get_mic(self, session_id, gss_kex=False): """ Create the MIC token for a SSH2 message. :param str session_id: The SSH session ID :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not :return: gssapi-with-mic: Returns the MIC token from GSS-API for the message we created with ``_ssh_build_mic``. gssapi-keyex: Returns the MIC token from GSS-API with the SSH session ID as message. :rtype: str """ self._session_id = session_id if not gss_kex: mic_field = self._ssh_build_mic( self._session_id, self._username, self._service, self._auth_method, ) mic_token = self._gss_ctxt.get_signature(mic_field) else: # for key exchange with gssapi-keyex mic_token = self._gss_srv_ctxt.get_signature(self._session_id) return mic_token def ssh_accept_sec_context(self, hostname, recv_token, username=None): """ Accept a GSS-API context (server mode). :param str hostname: The servers hostname :param str username: The name of the user who attempts to login :param str recv_token: The GSS-API Token received from the server, if it's not the initial call. :return: A ``String`` if the GSS-API has returned a token or ``None`` if no token was returned """ # hostname and username are not required for GSSAPI, but for SSPI self._gss_host = hostname self._username = username if self._gss_srv_ctxt is None: self._gss_srv_ctxt = gssapi.SecurityContext(usage="accept") token = self._gss_srv_ctxt.step(recv_token) self._gss_srv_ctxt_status = self._gss_srv_ctxt.complete return token def ssh_check_mic(self, mic_token, session_id, username=None): """ Verify the MIC token for a SSH2 message. :param str mic_token: The MIC token received from the client :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login :return: None if the MIC check was successful :raises: ``gssapi.exceptions.GSSError`` -- if the MIC check failed """ self._session_id = session_id self._username = username if self._username is not None: # server mode mic_field = self._ssh_build_mic( self._session_id, self._username, self._service, self._auth_method, ) self._gss_srv_ctxt.verify_signature(mic_field, mic_token) else: # for key exchange with gssapi-keyex # client mode self._gss_ctxt.verify_signature(self._session_id, mic_token) @property def credentials_delegated(self): """ Checks if credentials are delegated (server mode). :return: ``True`` if credentials are delegated, otherwise ``False`` :rtype: bool """ if self._gss_srv_ctxt.delegated_creds is not None: return True return False def save_client_creds(self, client_token): """ Save the Client token in a file. This is used by the SSH server to store the client credentials if credentials are delegated (server mode). :param str client_token: The GSS-API token received form the client :raises: ``NotImplementedError`` -- Credential delegation is currently not supported in server mode """ raise NotImplementedError class _SSH_SSPI(_SSH_GSSAuth): """ Implementation of the Microsoft SSPI Kerberos Authentication for SSH2. :see: `.GSSAuth` """ def __init__(self, auth_method, gss_deleg_creds): """ :param str auth_method: The name of the SSH authentication mechanism (gssapi-with-mic or gss-keyex) :param bool gss_deleg_creds: Delegate client credentials or not """ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) if self._gss_deleg_creds: self._gss_flags = ( sspicon.ISC_REQ_INTEGRITY | sspicon.ISC_REQ_MUTUAL_AUTH | sspicon.ISC_REQ_DELEGATE ) else: self._gss_flags = ( sspicon.ISC_REQ_INTEGRITY | sspicon.ISC_REQ_MUTUAL_AUTH ) def ssh_init_sec_context( self, target, desired_mech=None, username=None, recv_token=None ): """ Initialize a SSPI context. :param str username: The name of the user who attempts to login :param str target: The FQDN of the target to connect to :param str desired_mech: The negotiated SSPI mechanism ("pseudo negotiated" mechanism, because we support just the krb5 mechanism :-)) :param recv_token: The SSPI token received from the Server :raises: `.SSHException` -- Is raised if the desired mechanism of the client is not supported :return: A ``String`` if the SSPI has returned a token or ``None`` if no token was returned """ from pyasn1.codec.der import decoder self._username = username self._gss_host = target error = 0 targ_name = "host/" + self._gss_host if desired_mech is not None: mech, __ = decoder.decode(desired_mech) if mech.__str__() != self._krb5_mech: raise SSHException("Unsupported mechanism OID.") try: if recv_token is None: self._gss_ctxt = sspi.ClientAuth( "Kerberos", scflags=self._gss_flags, targetspn=targ_name ) error, token = self._gss_ctxt.authorize(recv_token) token = token[0].Buffer except pywintypes.error as e: e.strerror += ", Target: {}".format(self._gss_host) raise if error == 0: """ if the status is GSS_COMPLETE (error = 0) the context is fully established an we can set _gss_ctxt_status to True. """ self._gss_ctxt_status = True token = None """ You won't get another token if the context is fully established, so i set token to None instead of "" """ return token def ssh_get_mic(self, session_id, gss_kex=False): """ Create the MIC token for a SSH2 message. :param str session_id: The SSH session ID :param bool gss_kex: Generate the MIC for Key Exchange with SSPI or not :return: gssapi-with-mic: Returns the MIC token from SSPI for the message we created with ``_ssh_build_mic``. gssapi-keyex: Returns the MIC token from SSPI with the SSH session ID as message. """ self._session_id = session_id if not gss_kex: mic_field = self._ssh_build_mic( self._session_id, self._username, self._service, self._auth_method, ) mic_token = self._gss_ctxt.sign(mic_field) else: # for key exchange with gssapi-keyex mic_token = self._gss_srv_ctxt.sign(self._session_id) return mic_token def ssh_accept_sec_context(self, hostname, username, recv_token): """ Accept a SSPI context (server mode). :param str hostname: The servers FQDN :param str username: The name of the user who attempts to login :param str recv_token: The SSPI Token received from the server, if it's not the initial call. :return: A ``String`` if the SSPI has returned a token or ``None`` if no token was returned """ self._gss_host = hostname self._username = username targ_name = "host/" + self._gss_host self._gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=targ_name) error, token = self._gss_srv_ctxt.authorize(recv_token) token = token[0].Buffer if error == 0: self._gss_srv_ctxt_status = True token = None return token def ssh_check_mic(self, mic_token, session_id, username=None): """ Verify the MIC token for a SSH2 message. :param str mic_token: The MIC token received from the client :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login :return: None if the MIC check was successful :raises: ``sspi.error`` -- if the MIC check failed """ self._session_id = session_id self._username = username if username is not None: # server mode mic_field = self._ssh_build_mic( self._session_id, self._username, self._service, self._auth_method, ) # Verifies data and its signature. If verification fails, an # sspi.error will be raised. self._gss_srv_ctxt.verify(mic_field, mic_token) else: # for key exchange with gssapi-keyex # client mode # Verifies data and its signature. If verification fails, an # sspi.error will be raised. self._gss_ctxt.verify(self._session_id, mic_token) @property def credentials_delegated(self): """ Checks if credentials are delegated (server mode). :return: ``True`` if credentials are delegated, otherwise ``False`` """ return self._gss_flags & sspicon.ISC_REQ_DELEGATE and ( self._gss_srv_ctxt_status or self._gss_flags ) def save_client_creds(self, client_token): """ Save the Client token in a file. This is used by the SSH server to store the client credentails if credentials are delegated (server mode). :param str client_token: The SSPI token received form the client :raises: ``NotImplementedError`` -- Credential delegation is currently not supported in server mode """ raise NotImplementedError