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

247 statements  

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 stat 

26import struct 

27import sys 

28import tempfile 

29import threading 

30import time 

31from logging import DEBUG 

32from select import select 

33 

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 

39 

40cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11) 

41SSH2_AGENT_IDENTITIES_ANSWER = 12 

42cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13) 

43SSH2_AGENT_SIGN_RESPONSE = 14 

44 

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 

58 

59 

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

66 

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. 

72 

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

74 when the connection was made. 

75 

76 :return: 

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

78 SSH agent 

79 """ 

80 return self._keys 

81 

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) 

97 

98 def _close(self): 

99 if self._conn is not None: 

100 self._conn.close() 

101 self._conn = None 

102 self._keys = () 

103 

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 

110 

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 

121 

122 

123class AgentProxyThread(threading.Thread): 

124 """ 

125 Class in charge of communication between two channels. 

126 """ 

127 

128 def __init__(self, agent): 

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

130 self._agent = agent 

131 self._exit = False 

132 

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 

151 

152 def _communicate(self): 

153 import fcntl 

154 

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) 

175 

176 def _close(self): 

177 self._exit = True 

178 self.__inr.close() 

179 self._agent._conn.close() 

180 

181 

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

187 

188 def __init__(self, agent): 

189 AgentProxyThread.__init__(self, agent) 

190 

191 def get_connection(self): 

192 """ 

193 Return a pair of socket object and string address. 

194 

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 

205 

206 

207class AgentRemoteProxy(AgentProxyThread): 

208 """ 

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

210 """ 

211 

212 def __init__(self, agent, chan): 

213 AgentProxyThread.__init__(self, agent) 

214 self.__chan = chan 

215 

216 def get_connection(self): 

217 return self.__chan, None 

218 

219 

220def get_agent_connection(): 

221 """ 

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

223 

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 

236 

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 

246 

247 

248class AgentClientProxy: 

249 """ 

250 Class proxying request as a client: 

251 

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

260 

261 def __init__(self, chanRemote): 

262 self._conn = None 

263 self.__chanR = chanRemote 

264 self.thread = AgentRemoteProxy(self, chanRemote) 

265 self.thread.start() 

266 

267 def __del__(self): 

268 self.close() 

269 

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 

278 

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

289 

290 

291class AgentServerProxy(AgentSSH): 

292 """ 

293 Allows an SSH server to access a forwarded agent. 

294 

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. 

298 

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

303 

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

305 

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

307 """ 

308 

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

317 

318 def __del__(self): 

319 self.close() 

320 

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) 

327 

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

338 

339 def get_env(self): 

340 """ 

341 Helper for the environment under unix 

342 

343 :return: 

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

345 """ 

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

347 

348 def _get_filename(self): 

349 return self._file 

350 

351 

352class AgentRequestHandler: 

353 """ 

354 Primary/default implementation of SSH agent forwarding functionality. 

355 

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. 

359 

360 For example:: 

361 

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

373 

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

379 

380 def _forward_agent_handler(self, chanRemote): 

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

382 

383 def __del__(self): 

384 self.close() 

385 

386 def close(self): 

387 for p in self.__clientProxys: 

388 p.close() 

389 

390 

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. 

397 

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. 

401 

402 :raises: `.SSHException` -- 

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

404 

405 .. versionchanged:: 2.10 

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

407 putty pageant support) 

408 """ 

409 

410 def __init__(self): 

411 AgentSSH.__init__(self) 

412 

413 conn = get_agent_connection() 

414 if not conn: 

415 return 

416 self._connect(conn) 

417 

418 def close(self): 

419 """ 

420 Close the SSH agent connection. 

421 """ 

422 self._close() 

423 

424 

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. 

430 

431 .. versionchanged:: 3.2 

432 Added the ``comment`` kwarg and attribute. 

433 

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

438 

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

455 

456 def log(self, *args, **kwargs): 

457 return self._logger.log(*args, **kwargs) 

458 

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 

462 

463 def get_name(self): 

464 return self.name 

465 

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

471 

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) 

479 

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 

484 

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