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

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""" 

20SSH Agent interface 

21""" 

22 

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 

33 

34from paramiko.ssh_exception import SSHException, AuthenticationException 

35from paramiko.message import Message 

36from paramiko.pkey import PKey 

37from paramiko.util import asbytes 

38 

39cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11) 

40SSH2_AGENT_IDENTITIES_ANSWER = 12 

41cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13) 

42SSH2_AGENT_SIGN_RESPONSE = 14 

43 

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} 

55 

56 

57class AgentSSH: 

58 def __init__(self): 

59 self._conn = None 

60 self._keys = () 

61 

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. 

67 

68 This method performs no IO, just returns the list of keys retrieved 

69 when the connection was made. 

70 

71 :return: 

72 a tuple of `.AgentKey` objects representing keys available on the 

73 SSH agent 

74 """ 

75 return self._keys 

76 

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) 

87 

88 def _close(self): 

89 if self._conn is not None: 

90 self._conn.close() 

91 self._conn = None 

92 self._keys = () 

93 

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 

100 

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 

111 

112 

113class AgentProxyThread(threading.Thread): 

114 """ 

115 Class in charge of communication between two channels. 

116 """ 

117 

118 def __init__(self, agent): 

119 threading.Thread.__init__(self, target=self.run) 

120 self._agent = agent 

121 self._exit = False 

122 

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 

141 

142 def _communicate(self): 

143 import fcntl 

144 

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) 

165 

166 def _close(self): 

167 self._exit = True 

168 self.__inr.close() 

169 self._agent._conn.close() 

170 

171 

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 """ 

177 

178 def __init__(self, agent): 

179 AgentProxyThread.__init__(self, agent) 

180 

181 def get_connection(self): 

182 """ 

183 Return a pair of socket object and string address. 

184 

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 

195 

196 

197class AgentRemoteProxy(AgentProxyThread): 

198 """ 

199 Class to be used when wanting to ask a remote SSH Agent 

200 """ 

201 

202 def __init__(self, agent, chan): 

203 AgentProxyThread.__init__(self, agent) 

204 self.__chan = chan 

205 

206 def get_connection(self): 

207 return self.__chan, None 

208 

209 

210def get_agent_connection(): 

211 """ 

212 Returns some SSH agent object, or None if none were found/supported. 

213 

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 

226 

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 

236 

237 

238class AgentClientProxy: 

239 """ 

240 Class proxying request as a client: 

241 

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 """ 

250 

251 def __init__(self, chanRemote): 

252 self._conn = None 

253 self.__chanR = chanRemote 

254 self.thread = AgentRemoteProxy(self, chanRemote) 

255 self.thread.start() 

256 

257 def __del__(self): 

258 self.close() 

259 

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 

268 

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() 

279 

280 

281class AgentServerProxy(AgentSSH): 

282 """ 

283 Allows an SSH server to access a forwarded agent. 

284 

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. 

288 

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.) 

293 

294 :param .Transport t: Transport used for SSH Agent communication forwarding 

295 

296 :raises: `.SSHException` -- mostly if we lost the agent 

297 """ 

298 

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() 

307 

308 def __del__(self): 

309 self.close() 

310 

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) 

317 

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() 

328 

329 def get_env(self): 

330 """ 

331 Helper for the environment under unix 

332 

333 :return: 

334 a dict containing the ``SSH_AUTH_SOCK`` environment variables 

335 """ 

336 return {"SSH_AUTH_SOCK": self._get_filename()} 

337 

338 def _get_filename(self): 

339 return self._file 

340 

341 

342class AgentRequestHandler: 

343 """ 

344 Primary/default implementation of SSH agent forwarding functionality. 

345 

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. 

349 

350 For example:: 

351 

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 """ 

363 

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 = [] 

369 

370 def _forward_agent_handler(self, chanRemote): 

371 self.__clientProxys.append(AgentClientProxy(chanRemote)) 

372 

373 def __del__(self): 

374 self.close() 

375 

376 def close(self): 

377 for p in self.__clientProxys: 

378 p.close() 

379 

380 

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. 

387 

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. 

391 

392 :raises: `.SSHException` -- 

393 if an SSH agent is found, but speaks an incompatible protocol 

394 

395 .. versionchanged:: 2.10 

396 Added support for native openssh agent on windows (extending previous 

397 putty pageant support) 

398 """ 

399 

400 def __init__(self): 

401 AgentSSH.__init__(self) 

402 

403 conn = get_agent_connection() 

404 if not conn: 

405 return 

406 self._connect(conn) 

407 

408 def close(self): 

409 """ 

410 Close the SSH agent connection. 

411 """ 

412 self._close() 

413 

414 

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 """ 

421 

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() 

427 

428 def asbytes(self): 

429 return self.blob 

430 

431 def __str__(self): 

432 return self.asbytes() 

433 

434 def get_name(self): 

435 return self.name 

436 

437 @property 

438 def _fields(self): 

439 raise NotImplementedError 

440 

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()