Coverage for /pythoncovmergedfiles/medio/medio/src/paramiko/paramiko/agent.py: 30%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 logging import DEBUG
32from select import select
33from paramiko.common import io_sleep, byte_chr
35from paramiko.ssh_exception import SSHException, AuthenticationException
36from paramiko.message import Message
37from paramiko.pkey import PKey, UnknownKeyType
38from paramiko.util import asbytes, get_logger
40cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11)
41SSH2_AGENT_IDENTITIES_ANSWER = 12
42cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13)
43SSH2_AGENT_SIGN_RESPONSE = 14
45SSH_AGENT_RSA_SHA2_256 = 2
46SSH_AGENT_RSA_SHA2_512 = 4
47# NOTE: RFC mildly confusing; while these flags are OR'd together, OpenSSH at
48# least really treats them like "AND"s, in the sense that if it finds the
49# SHA256 flag set it won't continue looking at the SHA512 one; it
50# 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"] = value
60# TODO 4.0: rename all these - including making some of their methods public?
61class AgentSSH:
62 def __init__(self):
63 self._conn = None
64 self._keys = ()
66 def get_keys(self):
67 """
68 Return the list of keys available through the SSH agent, if any. If
69 no SSH agent was running (or it couldn't be contacted), an empty list
70 will be returned.
72 This method performs no IO, just returns the list of keys retrieved
73 when the connection was made.
75 :return:
76 a tuple of `.AgentKey` objects representing keys available on the
77 SSH agent
78 """
79 return self._keys
81 def _connect(self, conn):
82 self._conn = conn
83 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)
97 def _close(self):
98 if self._conn is not None:
99 self._conn.close()
100 self._conn = None
101 self._keys = ()
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()), msg
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 += extra
119 return result
122class AgentProxyThread(threading.Thread):
123 """
124 Class in charge of communication between two channels.
125 """
127 def __init__(self, agent):
128 threading.Thread.__init__(self, target=self.run)
129 self._agent = agent
130 self._exit = False
132 def run(self):
133 try:
134 (r, addr) = self.get_connection()
135 # Found that r should be either
136 # a socket from the socket library or None
137 self.__inr = r
138 # The address should be an IP address as a string? or None
139 self.__addr = addr
140 self._agent.connect()
141 if not isinstance(self._agent, int) and (
142 self._agent._conn is None
143 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 raise
151 def _communicate(self):
152 import fcntl
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 break
166 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 break
173 time.sleep(io_sleep)
175 def _close(self):
176 self._exit = True
177 self.__inr.close()
178 self._agent._conn.close()
181class AgentLocalProxy(AgentProxyThread):
182 """
183 Class to be used when wanting to ask a local SSH Agent being
184 asked from a remote fake agent (so use a unix socket for ex.)
185 """
187 def __init__(self, agent):
188 AgentProxyThread.__init__(self, agent)
190 def get_connection(self):
191 """
192 Return a pair of socket object and string address.
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, addr
202 except:
203 raise
206class AgentRemoteProxy(AgentProxyThread):
207 """
208 Class to be used when wanting to ask a remote SSH Agent
209 """
211 def __init__(self, agent, chan):
212 AgentProxyThread.__init__(self, agent)
213 self.__chan = chan
215 def get_connection(self):
216 return self.__chan, None
219def get_agent_connection():
220 """
221 Returns some SSH agent object, or None if none were found/supported.
223 .. versionadded:: 2.10
224 """
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 conn
230 except:
231 # probably a dangling env var: the ssh agent is gone
232 return
233 elif sys.platform == "win32":
234 from . import win_pageant, win_openssh
236 conn = None
237 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 conn
242 else:
243 # no agent support
244 return
247class AgentClientProxy:
248 """
249 Class proxying request as a client:
251 #. client ask for a request_forward_agent()
252 #. server creates a proxy and a fake SSH Agent
253 #. 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 connecting
256 the remote fake agent and the local agent
257 #. Communication occurs ...
258 """
260 def __init__(self, chanRemote):
261 self._conn = None
262 self.__chanR = chanRemote
263 self.thread = AgentRemoteProxy(self, chanRemote)
264 self.thread.start()
266 def __del__(self):
267 self.close()
269 def connect(self):
270 """
271 Method automatically called by ``AgentProxyThread.run``.
272 """
273 conn = get_agent_connection()
274 if not conn:
275 return
276 self._conn = conn
278 def close(self):
279 """
280 Close the current connection and terminate the agent
281 Should be called manually
282 """
283 if hasattr(self, "thread"):
284 self.thread._exit = True
285 self.thread.join(1000)
286 if self._conn is not None:
287 self._conn.close()
290class AgentServerProxy(AgentSSH):
291 """
292 Allows an SSH server to access a forwarded agent.
294 This also creates a unix domain socket on the system to allow external
295 programs to also access the agent. For this reason, you probably only want
296 to create one of these.
298 :meth:`connect` must be called before it is usable. This will also load the
299 list of keys the agent contains. You must also call :meth:`close` in
300 order to clean up the unix socket and the thread that maintains it.
301 (:class:`contextlib.closing` might be helpful to you.)
303 :param .Transport t: Transport used for SSH Agent communication forwarding
305 :raises: `.SSHException` -- mostly if we lost the agent
306 """
308 def __init__(self, t):
309 AgentSSH.__init__(self)
310 self.__t = t
311 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()
317 def __del__(self):
318 self.close()
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)
327 def close(self):
328 """
329 Terminate the agent, clean the files, close connections
330 Should be called manually
331 """
332 os.remove(self._file)
333 os.rmdir(self._dir)
334 self.thread._exit = True
335 self.thread.join(1000)
336 self._close()
338 def get_env(self):
339 """
340 Helper for the environment under unix
342 :return:
343 a dict containing the ``SSH_AUTH_SOCK`` environment variables
344 """
345 return {"SSH_AUTH_SOCK": self._get_filename()}
347 def _get_filename(self):
348 return self._file
351class AgentRequestHandler:
352 """
353 Primary/default implementation of SSH agent forwarding functionality.
355 Simply instantiate this class, handing it a live command-executing session
356 object, and it will handle forwarding any local SSH agent processes it
357 finds.
359 For example::
361 # Connect
362 client = SSHClient()
363 client.connect(host, port, username)
364 # Obtain session
365 session = client.get_transport().open_session()
366 # Forward local agent
367 AgentRequestHandler(session)
368 # Commands executed after this point will see the forwarded agent on
369 # the remote end.
370 session.exec_command("git clone https://my.git.repository/")
371 """
373 def __init__(self, chanClient):
374 self._conn = None
375 self.__chanC = chanClient
376 chanClient.request_forward_agent(self._forward_agent_handler)
377 self.__clientProxys = []
379 def _forward_agent_handler(self, chanRemote):
380 self.__clientProxys.append(AgentClientProxy(chanRemote))
382 def __del__(self):
383 self.close()
385 def close(self):
386 for p in self.__clientProxys:
387 p.close()
390class Agent(AgentSSH):
391 """
392 Client interface for using private keys from an SSH agent running on the
393 local machine. If an SSH agent is running, this class can be used to
394 connect to it and retrieve `.PKey` objects which can be used when
395 attempting to authenticate to remote SSH servers.
397 Upon initialization, a session with the local machine's SSH agent is
398 opened, if one is running. If no agent is running, initialization will
399 succeed, but `get_keys` will return an empty tuple.
401 :raises: `.SSHException` --
402 if an SSH agent is found, but speaks an incompatible protocol
404 .. versionchanged:: 2.10
405 Added support for native openssh agent on windows (extending previous
406 putty pageant support)
407 """
409 def __init__(self):
410 AgentSSH.__init__(self)
412 conn = get_agent_connection()
413 if not conn:
414 return
415 self._connect(conn)
417 def close(self):
418 """
419 Close the SSH agent connection.
420 """
421 self._close()
424class AgentKey(PKey):
425 """
426 Private key held in a local SSH agent. This type of key can be used for
427 authenticating to a remote server (signing). Most other key operations
428 work as expected.
430 .. versionchanged:: 3.2
431 Added the ``comment`` kwarg and attribute.
433 .. versionchanged:: 3.2
434 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 """
438 def __init__(self, agent, blob, comment=""):
439 self.agent = agent
440 self.blob = blob
441 self.comment = comment
442 msg = Message(blob)
443 self.name = msg.get_text()
444 self._logger = get_logger(__file__)
445 self.inner_key = None
446 try:
447 self.inner_key = PKey.from_type_string(
448 key_type=self.name, key_bytes=blob
449 )
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))
455 def log(self, *args, **kwargs):
456 return self._logger.log(*args, **kwargs)
458 def asbytes(self):
459 # Prefer inner_key.asbytes, since that will differ for eg RSA-CERT
460 return self.inner_key.asbytes() if self.inner_key else self.blob
462 def get_name(self):
463 return self.name
465 def get_bits(self):
466 # Have to work around PKey's default get_bits being crap
467 if self.inner_key is not None:
468 return self.inner_key.get_bits()
469 return super().get_bits()
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 to
476 raise AttributeError(name)
477 return getattr(self.inner_key, name)
479 @property
480 def _fields(self):
481 fallback = [self.get_name(), self.blob]
482 return self.inner_key._fields if self.inner_key else fallback
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 for
488 # RSA-CERT 'keys' - those end up always degrading to ssh-rsa type
489 # 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()