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