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 stat
26import struct
27import sys
28import tempfile
29import threading
30import time
31from logging import DEBUG
32from select import select
34from paramiko.common import byte_chr, io_sleep
35from paramiko.message import Message
36from paramiko.pkey import PKey, UnknownKeyType
37from paramiko.ssh_exception import AuthenticationException, SSHException
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 (backwards incompat): rename all these - including making some of their
61# methods public?
62class AgentSSH:
63 def __init__(self):
64 self._conn = None
65 self._keys = ()
67 def get_keys(self):
68 """
69 Return the list of keys available through the SSH agent, if any. If
70 no SSH agent was running (or it couldn't be contacted), an empty list
71 will be returned.
73 This method performs no IO, just returns the list of keys retrieved
74 when the connection was made.
76 :return:
77 a tuple of `.AgentKey` objects representing keys available on the
78 SSH agent
79 """
80 return self._keys
82 def _connect(self, conn):
83 self._conn = conn
84 ptype, result = self._send_message(cSSH2_AGENTC_REQUEST_IDENTITIES)
85 if ptype != SSH2_AGENT_IDENTITIES_ANSWER:
86 raise SSHException("could not get keys from ssh-agent")
87 keys = []
88 for i in range(result.get_int()):
89 keys.append(
90 AgentKey(
91 agent=self,
92 blob=result.get_binary(),
93 comment=result.get_text(),
94 )
95 )
96 self._keys = tuple(keys)
98 def _close(self):
99 if self._conn is not None:
100 self._conn.close()
101 self._conn = None
102 self._keys = ()
104 def _send_message(self, msg):
105 msg = asbytes(msg)
106 self._conn.send(struct.pack(">I", len(msg)) + msg)
107 data = self._read_all(4)
108 msg = Message(self._read_all(struct.unpack(">I", data)[0]))
109 return ord(msg.get_byte()), msg
111 def _read_all(self, wanted):
112 result = self._conn.recv(wanted)
113 while len(result) < wanted:
114 if len(result) == 0:
115 raise SSHException("lost ssh-agent")
116 extra = self._conn.recv(wanted - len(result))
117 if len(extra) == 0:
118 raise SSHException("lost ssh-agent")
119 result += extra
120 return result
123class AgentProxyThread(threading.Thread):
124 """
125 Class in charge of communication between two channels.
126 """
128 def __init__(self, agent):
129 threading.Thread.__init__(self, target=self.run)
130 self._agent = agent
131 self._exit = False
133 def run(self):
134 try:
135 (r, addr) = self.get_connection()
136 # Found that r should be either
137 # a socket from the socket library or None
138 self.__inr = r
139 # The address should be an IP address as a string? or None
140 self.__addr = addr
141 self._agent.connect()
142 if not isinstance(self._agent, int) and (
143 self._agent._conn is None
144 or not hasattr(self._agent._conn, "fileno")
145 ):
146 raise AuthenticationException("Unable to connect to SSH agent")
147 self._communicate()
148 except:
149 # XXX Not sure what to do here ... raise or pass ?
150 raise
152 def _communicate(self):
153 import fcntl
155 oldflags = fcntl.fcntl(self.__inr, fcntl.F_GETFL)
156 fcntl.fcntl(self.__inr, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
157 while not self._exit:
158 events = select([self._agent._conn, self.__inr], [], [], 0.5)
159 for fd in events[0]:
160 if self._agent._conn == fd:
161 data = self._agent._conn.recv(512)
162 if len(data) != 0:
163 self.__inr.send(data)
164 else:
165 self._close()
166 break
167 elif self.__inr == fd:
168 data = self.__inr.recv(512)
169 if len(data) != 0:
170 self._agent._conn.send(data)
171 else:
172 self._close()
173 break
174 time.sleep(io_sleep)
176 def _close(self):
177 self._exit = True
178 self.__inr.close()
179 self._agent._conn.close()
182class AgentLocalProxy(AgentProxyThread):
183 """
184 Class to be used when wanting to ask a local SSH Agent being
185 asked from a remote fake agent (so use a unix socket for ex.)
186 """
188 def __init__(self, agent):
189 AgentProxyThread.__init__(self, agent)
191 def get_connection(self):
192 """
193 Return a pair of socket object and string address.
195 May block!
196 """
197 conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
198 try:
199 conn.bind(self._agent._get_filename())
200 conn.listen(1)
201 (r, addr) = conn.accept()
202 return r, addr
203 except:
204 raise
207class AgentRemoteProxy(AgentProxyThread):
208 """
209 Class to be used when wanting to ask a remote SSH Agent
210 """
212 def __init__(self, agent, chan):
213 AgentProxyThread.__init__(self, agent)
214 self.__chan = chan
216 def get_connection(self):
217 return self.__chan, None
220def get_agent_connection():
221 """
222 Returns some SSH agent object, or None if none were found/supported.
224 .. versionadded:: 2.10
225 """
226 if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"):
227 conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
228 try:
229 conn.connect(os.environ["SSH_AUTH_SOCK"])
230 return conn
231 except:
232 # probably a dangling env var: the ssh agent is gone
233 return
234 elif sys.platform == "win32":
235 from . import win_openssh, win_pageant
237 conn = None
238 if win_pageant.can_talk_to_agent():
239 conn = win_pageant.PageantConnection()
240 elif win_openssh.can_talk_to_agent():
241 conn = win_openssh.OpenSSHAgentConnection()
242 return conn
243 else:
244 # no agent support
245 return
248class AgentClientProxy:
249 """
250 Class proxying request as a client:
252 #. client ask for a request_forward_agent()
253 #. server creates a proxy and a fake SSH Agent
254 #. server ask for establishing a connection when needed,
255 calling the forward_agent_handler at client side.
256 #. the forward_agent_handler launch a thread for connecting
257 the remote fake agent and the local agent
258 #. Communication occurs ...
259 """
261 def __init__(self, chanRemote):
262 self._conn = None
263 self.__chanR = chanRemote
264 self.thread = AgentRemoteProxy(self, chanRemote)
265 self.thread.start()
267 def __del__(self):
268 self.close()
270 def connect(self):
271 """
272 Method automatically called by ``AgentProxyThread.run``.
273 """
274 conn = get_agent_connection()
275 if not conn:
276 return
277 self._conn = conn
279 def close(self):
280 """
281 Close the current connection and terminate the agent
282 Should be called manually
283 """
284 if hasattr(self, "thread"):
285 self.thread._exit = True
286 self.thread.join(1000)
287 if self._conn is not None:
288 self._conn.close()
291class AgentServerProxy(AgentSSH):
292 """
293 Allows an SSH server to access a forwarded agent.
295 This also creates a unix domain socket on the system to allow external
296 programs to also access the agent. For this reason, you probably only want
297 to create one of these.
299 :meth:`connect` must be called before it is usable. This will also load the
300 list of keys the agent contains. You must also call :meth:`close` in
301 order to clean up the unix socket and the thread that maintains it.
302 (:class:`contextlib.closing` might be helpful to you.)
304 :param .Transport t: Transport used for SSH Agent communication forwarding
306 :raises: `.SSHException` -- mostly if we lost the agent
307 """
309 def __init__(self, t):
310 AgentSSH.__init__(self)
311 self.__t = t
312 self._dir = tempfile.mkdtemp("sshproxy")
313 os.chmod(self._dir, stat.S_IRWXU)
314 self._file = self._dir + "/sshproxy.ssh"
315 self.thread = AgentLocalProxy(self)
316 self.thread.start()
318 def __del__(self):
319 self.close()
321 def connect(self):
322 conn_sock = self.__t.open_forward_agent_channel()
323 if conn_sock is None:
324 raise SSHException("lost ssh-agent")
325 conn_sock.set_name("auth-agent")
326 self._connect(conn_sock)
328 def close(self):
329 """
330 Terminate the agent, clean the files, close connections
331 Should be called manually
332 """
333 os.remove(self._file)
334 os.rmdir(self._dir)
335 self.thread._exit = True
336 self.thread.join(1000)
337 self._close()
339 def get_env(self):
340 """
341 Helper for the environment under unix
343 :return:
344 a dict containing the ``SSH_AUTH_SOCK`` environment variables
345 """
346 return {"SSH_AUTH_SOCK": self._get_filename()}
348 def _get_filename(self):
349 return self._file
352class AgentRequestHandler:
353 """
354 Primary/default implementation of SSH agent forwarding functionality.
356 Simply instantiate this class, handing it a live command-executing session
357 object, and it will handle forwarding any local SSH agent processes it
358 finds.
360 For example::
362 # Connect
363 client = SSHClient()
364 client.connect(host, port, username)
365 # Obtain session
366 session = client.get_transport().open_session()
367 # Forward local agent
368 AgentRequestHandler(session)
369 # Commands executed after this point will see the forwarded agent on
370 # the remote end.
371 session.exec_command("git clone https://my.git.repository/")
372 """
374 def __init__(self, chanClient):
375 self._conn = None
376 self.__chanC = chanClient
377 chanClient.request_forward_agent(self._forward_agent_handler)
378 self.__clientProxys = []
380 def _forward_agent_handler(self, chanRemote):
381 self.__clientProxys.append(AgentClientProxy(chanRemote))
383 def __del__(self):
384 self.close()
386 def close(self):
387 for p in self.__clientProxys:
388 p.close()
391class Agent(AgentSSH):
392 """
393 Client interface for using private keys from an SSH agent running on the
394 local machine. If an SSH agent is running, this class can be used to
395 connect to it and retrieve `.PKey` objects which can be used when
396 attempting to authenticate to remote SSH servers.
398 Upon initialization, a session with the local machine's SSH agent is
399 opened, if one is running. If no agent is running, initialization will
400 succeed, but `get_keys` will return an empty tuple.
402 :raises: `.SSHException` --
403 if an SSH agent is found, but speaks an incompatible protocol
405 .. versionchanged:: 2.10
406 Added support for native openssh agent on windows (extending previous
407 putty pageant support)
408 """
410 def __init__(self):
411 AgentSSH.__init__(self)
413 conn = get_agent_connection()
414 if not conn:
415 return
416 self._connect(conn)
418 def close(self):
419 """
420 Close the SSH agent connection.
421 """
422 self._close()
425class AgentKey(PKey):
426 """
427 Private key held in a local SSH agent. This type of key can be used for
428 authenticating to a remote server (signing). Most other key operations
429 work as expected.
431 .. versionchanged:: 3.2
432 Added the ``comment`` kwarg and attribute.
434 .. versionchanged:: 3.2
435 Added the ``.inner_key`` attribute holding a reference to the 'real'
436 key instance this key is a proxy for, if one was obtainable, else None.
437 """
439 def __init__(self, agent: Agent, blob: bytes, comment: str = ""):
440 self.agent = agent
441 self.blob = blob
442 self.comment = comment
443 msg = Message(blob)
444 self.name = msg.get_text()
445 self._logger = get_logger(__file__)
446 self.inner_key = None
447 try:
448 self.inner_key = PKey.from_type_string(
449 key_type=self.name, key_bytes=blob
450 )
451 except UnknownKeyType:
452 # Log, but don't explode, since inner_key is a best-effort thing.
453 err = "Unable to derive inner_key for agent key of type {!r}"
454 self.log(DEBUG, err.format(self.name))
456 def log(self, *args, **kwargs):
457 return self._logger.log(*args, **kwargs)
459 def asbytes(self):
460 # Prefer inner_key.asbytes, since that will differ for eg RSA-CERT
461 return self.inner_key.asbytes() if self.inner_key else self.blob
463 def get_name(self):
464 return self.name
466 def get_bits(self):
467 # Have to work around PKey's default get_bits being crap
468 if self.inner_key is not None:
469 return self.inner_key.get_bits()
470 return super().get_bits()
472 def __getattr__(self, name):
473 """
474 Proxy any un-implemented methods/properties to the inner_key.
475 """
476 if self.inner_key is None: # nothing to proxy to
477 raise AttributeError(name)
478 return getattr(self.inner_key, name)
480 @property
481 def _fields(self):
482 fallback = [self.get_name(), self.blob]
483 return self.inner_key._fields if self.inner_key else fallback
485 def sign_ssh_data(self, data, algorithm=None):
486 msg = Message()
487 msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST)
488 # NOTE: this used to be just self.blob, which is not entirely right for
489 # RSA-CERT 'keys' - those end up always degrading to ssh-rsa type
490 # signatures, for reasons probably internal to OpenSSH's agent code,
491 # even if everything else wants SHA2 (including our flag map).
492 msg.add_string(self.asbytes())
493 msg.add_string(data)
494 msg.add_int(ALGORITHM_FLAG_MAP.get(algorithm, 0))
495 ptype, result = self.agent._send_message(msg)
496 if ptype != SSH2_AGENT_SIGN_RESPONSE:
497 raise SSHException("key cannot be used for signing")
498 return result.get_binary()