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

34 

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 

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

65 

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. 

71 

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

73 when the connection was made. 

74 

75 :return: 

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

77 SSH agent 

78 """ 

79 return self._keys 

80 

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) 

96 

97 def _close(self): 

98 if self._conn is not None: 

99 self._conn.close() 

100 self._conn = None 

101 self._keys = () 

102 

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 

109 

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 

120 

121 

122class AgentProxyThread(threading.Thread): 

123 """ 

124 Class in charge of communication between two channels. 

125 """ 

126 

127 def __init__(self, agent): 

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

129 self._agent = agent 

130 self._exit = False 

131 

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 

150 

151 def _communicate(self): 

152 import fcntl 

153 

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) 

174 

175 def _close(self): 

176 self._exit = True 

177 self.__inr.close() 

178 self._agent._conn.close() 

179 

180 

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

186 

187 def __init__(self, agent): 

188 AgentProxyThread.__init__(self, agent) 

189 

190 def get_connection(self): 

191 """ 

192 Return a pair of socket object and string address. 

193 

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 

204 

205 

206class AgentRemoteProxy(AgentProxyThread): 

207 """ 

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

209 """ 

210 

211 def __init__(self, agent, chan): 

212 AgentProxyThread.__init__(self, agent) 

213 self.__chan = chan 

214 

215 def get_connection(self): 

216 return self.__chan, None 

217 

218 

219def get_agent_connection(): 

220 """ 

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

222 

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 

235 

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 

245 

246 

247class AgentClientProxy: 

248 """ 

249 Class proxying request as a client: 

250 

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

259 

260 def __init__(self, chanRemote): 

261 self._conn = None 

262 self.__chanR = chanRemote 

263 self.thread = AgentRemoteProxy(self, chanRemote) 

264 self.thread.start() 

265 

266 def __del__(self): 

267 self.close() 

268 

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 

277 

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

288 

289 

290class AgentServerProxy(AgentSSH): 

291 """ 

292 Allows an SSH server to access a forwarded agent. 

293 

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. 

297 

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

302 

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

304 

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

306 """ 

307 

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

316 

317 def __del__(self): 

318 self.close() 

319 

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) 

326 

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

337 

338 def get_env(self): 

339 """ 

340 Helper for the environment under unix 

341 

342 :return: 

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

344 """ 

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

346 

347 def _get_filename(self): 

348 return self._file 

349 

350 

351class AgentRequestHandler: 

352 """ 

353 Primary/default implementation of SSH agent forwarding functionality. 

354 

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. 

358 

359 For example:: 

360 

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

372 

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

378 

379 def _forward_agent_handler(self, chanRemote): 

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

381 

382 def __del__(self): 

383 self.close() 

384 

385 def close(self): 

386 for p in self.__clientProxys: 

387 p.close() 

388 

389 

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. 

396 

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. 

400 

401 :raises: `.SSHException` -- 

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

403 

404 .. versionchanged:: 2.10 

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

406 putty pageant support) 

407 """ 

408 

409 def __init__(self): 

410 AgentSSH.__init__(self) 

411 

412 conn = get_agent_connection() 

413 if not conn: 

414 return 

415 self._connect(conn) 

416 

417 def close(self): 

418 """ 

419 Close the SSH agent connection. 

420 """ 

421 self._close() 

422 

423 

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. 

429 

430 .. versionchanged:: 3.2 

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

432 

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

437 

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

454 

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

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

457 

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 

461 

462 def get_name(self): 

463 return self.name 

464 

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

470 

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) 

478 

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 

483 

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