codekingpro/portable-devtools
114k
1# Copyright (C) 2003-2007 John Rochester <john@jrochester.org>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"""20SSH Agent interface21"""22 23import os24import socket25import struct26import sys27import threading28import time29import tempfile30import stat31from logging import DEBUG32from select import select33from paramiko.common import io_sleep, byte_chr34 35from paramiko.ssh_exception import SSHException, AuthenticationException36from paramiko.message import Message37from paramiko.pkey import PKey, UnknownKeyType38from paramiko.util import asbytes, get_logger39 40cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11)41SSH2_AGENT_IDENTITIES_ANSWER = 1242cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13)43SSH2_AGENT_SIGN_RESPONSE = 1444 45SSH_AGENT_RSA_SHA2_256 = 246SSH_AGENT_RSA_SHA2_512 = 447# NOTE: RFC mildly confusing; while these flags are OR'd together, OpenSSH at48# least really treats them like "AND"s, in the sense that if it finds the49# SHA256 flag set it won't continue looking at the SHA512 one; it50# short-circuits right away.51# Thus, we never want to eg submit 6 to say "either's good".52ALGORITHM_FLAG_MAP = {53 "rsa-sha2-256": SSH_AGENT_RSA_SHA2_256,54 "rsa-sha2-512": SSH_AGENT_RSA_SHA2_512,55}56for key, value in list(ALGORITHM_FLAG_MAP.items()):57 ALGORITHM_FLAG_MAP[f"{key}-cert-v01@openssh.com"] = value58 59 60# TODO 4.0: rename all these - including making some of their methods public?61class AgentSSH:62 def __init__(self):63 self._conn = None64 self._keys = ()65 66 def get_keys(self):67 """68 Return the list of keys available through the SSH agent, if any. If69 no SSH agent was running (or it couldn't be contacted), an empty list70 will be returned.71 72 This method performs no IO, just returns the list of keys retrieved73 when the connection was made.74 75 :return:76 a tuple of `.AgentKey` objects representing keys available on the77 SSH agent78 """79 return self._keys80 81 def _connect(self, conn):82 self._conn = conn83 ptype, result = self._send_message(cSSH2_AGENTC_REQUEST_IDENTITIES)84 if ptype != SSH2_AGENT_IDENTITIES_ANSWER:85 raise SSHException("could not get keys from ssh-agent")86 keys = []87 for i in range(result.get_int()):88 keys.append(89 AgentKey(90 agent=self,91 blob=result.get_binary(),92 comment=result.get_text(),93 )94 )95 self._keys = tuple(keys)96 97 def _close(self):98 if self._conn is not None:99 self._conn.close()100 self._conn = None101 self._keys = ()102 103 def _send_message(self, msg):104 msg = asbytes(msg)105 self._conn.send(struct.pack(">I", len(msg)) + msg)106 data = self._read_all(4)107 msg = Message(self._read_all(struct.unpack(">I", data)[0]))108 return ord(msg.get_byte()), msg109 110 def _read_all(self, wanted):111 result = self._conn.recv(wanted)112 while len(result) < wanted:113 if len(result) == 0:114 raise SSHException("lost ssh-agent")115 extra = self._conn.recv(wanted - len(result))116 if len(extra) == 0:117 raise SSHException("lost ssh-agent")118 result += extra119 return result120 121 122class AgentProxyThread(threading.Thread):123 """124 Class in charge of communication between two channels.125 """126 127 def __init__(self, agent):128 threading.Thread.__init__(self, target=self.run)129 self._agent = agent130 self._exit = False131 132 def run(self):133 try:134 (r, addr) = self.get_connection()135 # Found that r should be either136 # a socket from the socket library or None137 self.__inr = r138 # The address should be an IP address as a string? or None139 self.__addr = addr140 self._agent.connect()141 if not isinstance(self._agent, int) and (142 self._agent._conn is None143 or not hasattr(self._agent._conn, "fileno")144 ):145 raise AuthenticationException("Unable to connect to SSH agent")146 self._communicate()147 except:148 # XXX Not sure what to do here ... raise or pass ?149 raise150 151 def _communicate(self):152 import fcntl153 154 oldflags = fcntl.fcntl(self.__inr, fcntl.F_GETFL)155 fcntl.fcntl(self.__inr, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)156 while not self._exit:157 events = select([self._agent._conn, self.__inr], [], [], 0.5)158 for fd in events[0]:159 if self._agent._conn == fd:160 data = self._agent._conn.recv(512)161 if len(data) != 0:162 self.__inr.send(data)163 else:164 self._close()165 break166 elif self.__inr == fd:167 data = self.__inr.recv(512)168 if len(data) != 0:169 self._agent._conn.send(data)170 else:171 self._close()172 break173 time.sleep(io_sleep)174 175 def _close(self):176 self._exit = True177 self.__inr.close()178 self._agent._conn.close()179 180 181class AgentLocalProxy(AgentProxyThread):182 """183 Class to be used when wanting to ask a local SSH Agent being184 asked from a remote fake agent (so use a unix socket for ex.)185 """186 187 def __init__(self, agent):188 AgentProxyThread.__init__(self, agent)189 190 def get_connection(self):191 """192 Return a pair of socket object and string address.193 194 May block!195 """196 conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)197 try:198 conn.bind(self._agent._get_filename())199 conn.listen(1)200 (r, addr) = conn.accept()201 return r, addr202 except:203 raise204 205 206class AgentRemoteProxy(AgentProxyThread):207 """208 Class to be used when wanting to ask a remote SSH Agent209 """210 211 def __init__(self, agent, chan):212 AgentProxyThread.__init__(self, agent)213 self.__chan = chan214 215 def get_connection(self):216 return self.__chan, None217 218 219def get_agent_connection():220 """221 Returns some SSH agent object, or None if none were found/supported.222 223 .. versionadded:: 2.10224 """225 if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"):226 conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)227 try:228 conn.connect(os.environ["SSH_AUTH_SOCK"])229 return conn230 except:231 # probably a dangling env var: the ssh agent is gone232 return233 elif sys.platform == "win32":234 from . import win_pageant, win_openssh235 236 conn = None237 if win_pageant.can_talk_to_agent():238 conn = win_pageant.PageantConnection()239 elif win_openssh.can_talk_to_agent():240 conn = win_openssh.OpenSSHAgentConnection()241 return conn242 else:243 # no agent support244 return245 246 247class AgentClientProxy:248 """249 Class proxying request as a client:250 251 #. client ask for a request_forward_agent()252 #. server creates a proxy and a fake SSH Agent253 #. server ask for establishing a connection when needed,254 calling the forward_agent_handler at client side.255 #. the forward_agent_handler launch a thread for connecting256 the remote fake agent and the local agent257 #. Communication occurs ...258 """259 260 def __init__(self, chanRemote):261 self._conn = None262 self.__chanR = chanRemote263 self.thread = AgentRemoteProxy(self, chanRemote)264 self.thread.start()265 266 def __del__(self):267 self.close()268 269 def connect(self):270 """271 Method automatically called by ``AgentProxyThread.run``.272 """273 conn = get_agent_connection()274 if not conn:275 return276 self._conn = conn277 278 def close(self):279 """280 Close the current connection and terminate the agent281 Should be called manually282 """283 if hasattr(self, "thread"):284 self.thread._exit = True285 self.thread.join(1000)286 if self._conn is not None:287 self._conn.close()288 289 290class AgentServerProxy(AgentSSH):291 """292 Allows an SSH server to access a forwarded agent.293 294 This also creates a unix domain socket on the system to allow external295 programs to also access the agent. For this reason, you probably only want296 to create one of these.297 298 :meth:`connect` must be called before it is usable. This will also load the299 list of keys the agent contains. You must also call :meth:`close` in300 order to clean up the unix socket and the thread that maintains it.301 (:class:`contextlib.closing` might be helpful to you.)302 303 :param .Transport t: Transport used for SSH Agent communication forwarding304 305 :raises: `.SSHException` -- mostly if we lost the agent306 """307 308 def __init__(self, t):309 AgentSSH.__init__(self)310 self.__t = t311 self._dir = tempfile.mkdtemp("sshproxy")312 os.chmod(self._dir, stat.S_IRWXU)313 self._file = self._dir + "/sshproxy.ssh"314 self.thread = AgentLocalProxy(self)315 self.thread.start()316 317 def __del__(self):318 self.close()319 320 def connect(self):321 conn_sock = self.__t.open_forward_agent_channel()322 if conn_sock is None:323 raise SSHException("lost ssh-agent")324 conn_sock.set_name("auth-agent")325 self._connect(conn_sock)326 327 def close(self):328 """329 Terminate the agent, clean the files, close connections330 Should be called manually331 """332 os.remove(self._file)333 os.rmdir(self._dir)334 self.thread._exit = True335 self.thread.join(1000)336 self._close()337 338 def get_env(self):339 """340 Helper for the environment under unix341 342 :return:343 a dict containing the ``SSH_AUTH_SOCK`` environment variables344 """345 return {"SSH_AUTH_SOCK": self._get_filename()}346 347 def _get_filename(self):348 return self._file349 350 351class AgentRequestHandler:352 """353 Primary/default implementation of SSH agent forwarding functionality.354 355 Simply instantiate this class, handing it a live command-executing session356 object, and it will handle forwarding any local SSH agent processes it357 finds.358 359 For example::360 361 # Connect362 client = SSHClient()363 client.connect(host, port, username)364 # Obtain session365 session = client.get_transport().open_session()366 # Forward local agent367 AgentRequestHandler(session)368 # Commands executed after this point will see the forwarded agent on369 # the remote end.370 session.exec_command("git clone https://my.git.repository/")371 """372 373 def __init__(self, chanClient):374 self._conn = None375 self.__chanC = chanClient376 chanClient.request_forward_agent(self._forward_agent_handler)377 self.__clientProxys = []378 379 def _forward_agent_handler(self, chanRemote):380 self.__clientProxys.append(AgentClientProxy(chanRemote))381 382 def __del__(self):383 self.close()384 385 def close(self):386 for p in self.__clientProxys:387 p.close()388 389 390class Agent(AgentSSH):391 """392 Client interface for using private keys from an SSH agent running on the393 local machine. If an SSH agent is running, this class can be used to394 connect to it and retrieve `.PKey` objects which can be used when395 attempting to authenticate to remote SSH servers.396 397 Upon initialization, a session with the local machine's SSH agent is398 opened, if one is running. If no agent is running, initialization will399 succeed, but `get_keys` will return an empty tuple.400 401 :raises: `.SSHException` --402 if an SSH agent is found, but speaks an incompatible protocol403 404 .. versionchanged:: 2.10405 Added support for native openssh agent on windows (extending previous406 putty pageant support)407 """408 409 def __init__(self):410 AgentSSH.__init__(self)411 412 conn = get_agent_connection()413 if not conn:414 return415 self._connect(conn)416 417 def close(self):418 """419 Close the SSH agent connection.420 """421 self._close()422 423 424class AgentKey(PKey):425 """426 Private key held in a local SSH agent. This type of key can be used for427 authenticating to a remote server (signing). Most other key operations428 work as expected.429 430 .. versionchanged:: 3.2431 Added the ``comment`` kwarg and attribute.432 433 .. versionchanged:: 3.2434 Added the ``.inner_key`` attribute holding a reference to the 'real'435 key instance this key is a proxy for, if one was obtainable, else None.436 """437 438 def __init__(self, agent, blob, comment=""):439 self.agent = agent440 self.blob = blob441 self.comment = comment442 msg = Message(blob)443 self.name = msg.get_text()444 self._logger = get_logger(__file__)445 self.inner_key = None446 try:447 self.inner_key = PKey.from_type_string(448 key_type=self.name, key_bytes=blob449 )450 except UnknownKeyType:451 # Log, but don't explode, since inner_key is a best-effort thing.452 err = "Unable to derive inner_key for agent key of type {!r}"453 self.log(DEBUG, err.format(self.name))454 455 def log(self, *args, **kwargs):456 return self._logger.log(*args, **kwargs)457 458 def asbytes(self):459 # Prefer inner_key.asbytes, since that will differ for eg RSA-CERT460 return self.inner_key.asbytes() if self.inner_key else self.blob461 462 def get_name(self):463 return self.name464 465 def get_bits(self):466 # Have to work around PKey's default get_bits being crap467 if self.inner_key is not None:468 return self.inner_key.get_bits()469 return super().get_bits()470 471 def __getattr__(self, name):472 """473 Proxy any un-implemented methods/properties to the inner_key.474 """475 if self.inner_key is None: # nothing to proxy to476 raise AttributeError(name)477 return getattr(self.inner_key, name)478 479 @property480 def _fields(self):481 fallback = [self.get_name(), self.blob]482 return self.inner_key._fields if self.inner_key else fallback483 484 def sign_ssh_data(self, data, algorithm=None):485 msg = Message()486 msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST)487 # NOTE: this used to be just self.blob, which is not entirely right for488 # RSA-CERT 'keys' - those end up always degrading to ssh-rsa type489 # signatures, for reasons probably internal to OpenSSH's agent code,490 # even if everything else wants SHA2 (including our flag map).491 msg.add_string(self.asbytes())492 msg.add_string(data)493 msg.add_int(ALGORITHM_FLAG_MAP.get(algorithm, 0))494 ptype, result = self.agent._send_message(msg)495 if ptype != SSH2_AGENT_SIGN_RESPONSE:496 raise SSHException("key cannot be used for signing")497 return result.get_binary()498 