agent.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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 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. SSH Agent interface
  20. """
  21. import os
  22. import socket
  23. import struct
  24. import sys
  25. import threading
  26. import time
  27. import tempfile
  28. import stat
  29. from select import select
  30. from paramiko.common import asbytes, io_sleep
  31. from paramiko.py3compat import byte_chr
  32. from paramiko.ssh_exception import SSHException, AuthenticationException
  33. from paramiko.message import Message
  34. from paramiko.pkey import PKey
  35. from paramiko.util import retry_on_signal
  36. cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11)
  37. SSH2_AGENT_IDENTITIES_ANSWER = 12
  38. cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13)
  39. SSH2_AGENT_SIGN_RESPONSE = 14
  40. SSH_AGENT_RSA_SHA2_256 = 2
  41. SSH_AGENT_RSA_SHA2_512 = 4
  42. # NOTE: RFC mildly confusing; while these flags are OR'd together, OpenSSH at
  43. # least really treats them like "AND"s, in the sense that if it finds the
  44. # SHA256 flag set it won't continue looking at the SHA512 one; it
  45. # short-circuits right away.
  46. # Thus, we never want to eg submit 6 to say "either's good".
  47. ALGORITHM_FLAG_MAP = {
  48. "rsa-sha2-256": SSH_AGENT_RSA_SHA2_256,
  49. "rsa-sha2-512": SSH_AGENT_RSA_SHA2_512,
  50. }
  51. class AgentSSH(object):
  52. def __init__(self):
  53. self._conn = None
  54. self._keys = ()
  55. def get_keys(self):
  56. """
  57. Return the list of keys available through the SSH agent, if any. If
  58. no SSH agent was running (or it couldn't be contacted), an empty list
  59. will be returned.
  60. :return:
  61. a tuple of `.AgentKey` objects representing keys available on the
  62. SSH agent
  63. """
  64. return self._keys
  65. def _connect(self, conn):
  66. self._conn = conn
  67. ptype, result = self._send_message(cSSH2_AGENTC_REQUEST_IDENTITIES)
  68. if ptype != SSH2_AGENT_IDENTITIES_ANSWER:
  69. raise SSHException("could not get keys from ssh-agent")
  70. keys = []
  71. for i in range(result.get_int()):
  72. keys.append(AgentKey(self, result.get_binary()))
  73. result.get_string()
  74. self._keys = tuple(keys)
  75. def _close(self):
  76. if self._conn is not None:
  77. self._conn.close()
  78. self._conn = None
  79. self._keys = ()
  80. def _send_message(self, msg):
  81. msg = asbytes(msg)
  82. self._conn.send(struct.pack(">I", len(msg)) + msg)
  83. data = self._read_all(4)
  84. msg = Message(self._read_all(struct.unpack(">I", data)[0]))
  85. return ord(msg.get_byte()), msg
  86. def _read_all(self, wanted):
  87. result = self._conn.recv(wanted)
  88. while len(result) < wanted:
  89. if len(result) == 0:
  90. raise SSHException("lost ssh-agent")
  91. extra = self._conn.recv(wanted - len(result))
  92. if len(extra) == 0:
  93. raise SSHException("lost ssh-agent")
  94. result += extra
  95. return result
  96. class AgentProxyThread(threading.Thread):
  97. """
  98. Class in charge of communication between two channels.
  99. """
  100. def __init__(self, agent):
  101. threading.Thread.__init__(self, target=self.run)
  102. self._agent = agent
  103. self._exit = False
  104. def run(self):
  105. try:
  106. (r, addr) = self.get_connection()
  107. # Found that r should be either
  108. # a socket from the socket library or None
  109. self.__inr = r
  110. # The address should be an IP address as a string? or None
  111. self.__addr = addr
  112. self._agent.connect()
  113. if not isinstance(self._agent, int) and (
  114. self._agent._conn is None
  115. or not hasattr(self._agent._conn, "fileno")
  116. ):
  117. raise AuthenticationException("Unable to connect to SSH agent")
  118. self._communicate()
  119. except:
  120. # XXX Not sure what to do here ... raise or pass ?
  121. raise
  122. def _communicate(self):
  123. import fcntl
  124. oldflags = fcntl.fcntl(self.__inr, fcntl.F_GETFL)
  125. fcntl.fcntl(self.__inr, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
  126. while not self._exit:
  127. events = select([self._agent._conn, self.__inr], [], [], 0.5)
  128. for fd in events[0]:
  129. if self._agent._conn == fd:
  130. data = self._agent._conn.recv(512)
  131. if len(data) != 0:
  132. self.__inr.send(data)
  133. else:
  134. self._close()
  135. break
  136. elif self.__inr == fd:
  137. data = self.__inr.recv(512)
  138. if len(data) != 0:
  139. self._agent._conn.send(data)
  140. else:
  141. self._close()
  142. break
  143. time.sleep(io_sleep)
  144. def _close(self):
  145. self._exit = True
  146. self.__inr.close()
  147. self._agent._conn.close()
  148. class AgentLocalProxy(AgentProxyThread):
  149. """
  150. Class to be used when wanting to ask a local SSH Agent being
  151. asked from a remote fake agent (so use a unix socket for ex.)
  152. """
  153. def __init__(self, agent):
  154. AgentProxyThread.__init__(self, agent)
  155. def get_connection(self):
  156. """
  157. Return a pair of socket object and string address.
  158. May block!
  159. """
  160. conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  161. try:
  162. conn.bind(self._agent._get_filename())
  163. conn.listen(1)
  164. (r, addr) = conn.accept()
  165. return r, addr
  166. except:
  167. raise
  168. class AgentRemoteProxy(AgentProxyThread):
  169. """
  170. Class to be used when wanting to ask a remote SSH Agent
  171. """
  172. def __init__(self, agent, chan):
  173. AgentProxyThread.__init__(self, agent)
  174. self.__chan = chan
  175. def get_connection(self):
  176. return self.__chan, None
  177. def get_agent_connection():
  178. """
  179. Returns some SSH agent object, or None if none were found/supported.
  180. .. versionadded:: 2.10
  181. """
  182. if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"):
  183. conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  184. try:
  185. retry_on_signal(lambda: conn.connect(os.environ["SSH_AUTH_SOCK"]))
  186. return conn
  187. except:
  188. # probably a dangling env var: the ssh agent is gone
  189. return
  190. elif sys.platform == "win32":
  191. from . import win_pageant, win_openssh
  192. conn = None
  193. if win_pageant.can_talk_to_agent():
  194. conn = win_pageant.PageantConnection()
  195. elif win_openssh.can_talk_to_agent():
  196. conn = win_openssh.OpenSSHAgentConnection()
  197. return conn
  198. else:
  199. # no agent support
  200. return
  201. class AgentClientProxy(object):
  202. """
  203. Class proxying request as a client:
  204. #. client ask for a request_forward_agent()
  205. #. server creates a proxy and a fake SSH Agent
  206. #. server ask for establishing a connection when needed,
  207. calling the forward_agent_handler at client side.
  208. #. the forward_agent_handler launch a thread for connecting
  209. the remote fake agent and the local agent
  210. #. Communication occurs ...
  211. """
  212. def __init__(self, chanRemote):
  213. self._conn = None
  214. self.__chanR = chanRemote
  215. self.thread = AgentRemoteProxy(self, chanRemote)
  216. self.thread.start()
  217. def __del__(self):
  218. self.close()
  219. def connect(self):
  220. """
  221. Method automatically called by ``AgentProxyThread.run``.
  222. """
  223. conn = get_agent_connection()
  224. if not conn:
  225. return
  226. self._conn = conn
  227. def close(self):
  228. """
  229. Close the current connection and terminate the agent
  230. Should be called manually
  231. """
  232. if hasattr(self, "thread"):
  233. self.thread._exit = True
  234. self.thread.join(1000)
  235. if self._conn is not None:
  236. self._conn.close()
  237. class AgentServerProxy(AgentSSH):
  238. """
  239. :param .Transport t: Transport used for SSH Agent communication forwarding
  240. :raises: `.SSHException` -- mostly if we lost the agent
  241. """
  242. def __init__(self, t):
  243. AgentSSH.__init__(self)
  244. self.__t = t
  245. self._dir = tempfile.mkdtemp("sshproxy")
  246. os.chmod(self._dir, stat.S_IRWXU)
  247. self._file = self._dir + "/sshproxy.ssh"
  248. self.thread = AgentLocalProxy(self)
  249. self.thread.start()
  250. def __del__(self):
  251. self.close()
  252. def connect(self):
  253. conn_sock = self.__t.open_forward_agent_channel()
  254. if conn_sock is None:
  255. raise SSHException("lost ssh-agent")
  256. conn_sock.set_name("auth-agent")
  257. self._connect(conn_sock)
  258. def close(self):
  259. """
  260. Terminate the agent, clean the files, close connections
  261. Should be called manually
  262. """
  263. os.remove(self._file)
  264. os.rmdir(self._dir)
  265. self.thread._exit = True
  266. self.thread.join(1000)
  267. self._close()
  268. def get_env(self):
  269. """
  270. Helper for the environnement under unix
  271. :return:
  272. a dict containing the ``SSH_AUTH_SOCK`` environnement variables
  273. """
  274. return {"SSH_AUTH_SOCK": self._get_filename()}
  275. def _get_filename(self):
  276. return self._file
  277. class AgentRequestHandler(object):
  278. """
  279. Primary/default implementation of SSH agent forwarding functionality.
  280. Simply instantiate this class, handing it a live command-executing session
  281. object, and it will handle forwarding any local SSH agent processes it
  282. finds.
  283. For example::
  284. # Connect
  285. client = SSHClient()
  286. client.connect(host, port, username)
  287. # Obtain session
  288. session = client.get_transport().open_session()
  289. # Forward local agent
  290. AgentRequestHandler(session)
  291. # Commands executed after this point will see the forwarded agent on
  292. # the remote end.
  293. session.exec_command("git clone https://my.git.repository/")
  294. """
  295. def __init__(self, chanClient):
  296. self._conn = None
  297. self.__chanC = chanClient
  298. chanClient.request_forward_agent(self._forward_agent_handler)
  299. self.__clientProxys = []
  300. def _forward_agent_handler(self, chanRemote):
  301. self.__clientProxys.append(AgentClientProxy(chanRemote))
  302. def __del__(self):
  303. self.close()
  304. def close(self):
  305. for p in self.__clientProxys:
  306. p.close()
  307. class Agent(AgentSSH):
  308. """
  309. Client interface for using private keys from an SSH agent running on the
  310. local machine. If an SSH agent is running, this class can be used to
  311. connect to it and retrieve `.PKey` objects which can be used when
  312. attempting to authenticate to remote SSH servers.
  313. Upon initialization, a session with the local machine's SSH agent is
  314. opened, if one is running. If no agent is running, initialization will
  315. succeed, but `get_keys` will return an empty tuple.
  316. :raises: `.SSHException` --
  317. if an SSH agent is found, but speaks an incompatible protocol
  318. .. versionchanged:: 2.10
  319. Added support for native openssh agent on windows (extending previous
  320. putty pageant support)
  321. """
  322. def __init__(self):
  323. AgentSSH.__init__(self)
  324. conn = get_agent_connection()
  325. if not conn:
  326. return
  327. self._connect(conn)
  328. def close(self):
  329. """
  330. Close the SSH agent connection.
  331. """
  332. self._close()
  333. class AgentKey(PKey):
  334. """
  335. Private key held in a local SSH agent. This type of key can be used for
  336. authenticating to a remote server (signing). Most other key operations
  337. work as expected.
  338. """
  339. def __init__(self, agent, blob):
  340. self.agent = agent
  341. self.blob = blob
  342. self.public_blob = None
  343. self.name = Message(blob).get_text()
  344. def asbytes(self):
  345. return self.blob
  346. def __str__(self):
  347. return self.asbytes()
  348. def get_name(self):
  349. return self.name
  350. @property
  351. def _fields(self):
  352. raise NotImplementedError
  353. def sign_ssh_data(self, data, algorithm=None):
  354. msg = Message()
  355. msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST)
  356. msg.add_string(self.blob)
  357. msg.add_string(data)
  358. msg.add_int(ALGORITHM_FLAG_MAP.get(algorithm, 0))
  359. ptype, result = self.agent._send_message(msg)
  360. if ptype != SSH2_AGENT_SIGN_RESPONSE:
  361. raise SSHException("key cannot be used for signing")
  362. return result.get_binary()