From ae18228d079224d71b20846b9c4bcc2a5537e135 Mon Sep 17 00:00:00 2001 From: Robey Pointer Date: Thu, 7 Jul 2005 01:35:31 +0000 Subject: [PATCH] [project @ Arch-1:robey@lag.net--2005-master-shake%paramiko--dev--1--patch-26] new ssh agent support! from john rochester. added a bunch of docs to it, and changed demo.py to use an Agent if it finds a working key there. --- demo.py | 79 +++++++++++++++--------- paramiko/__init__.py | 4 +- paramiko/agent.py | 133 ++++++++++++++++++++++++++++++++++++++++ paramiko/sftp_client.py | 2 +- 4 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 paramiko/agent.py diff --git a/demo.py b/demo.py index 691b6f9..b85fc01 100755 --- a/demo.py +++ b/demo.py @@ -50,6 +50,51 @@ def load_host_keys(): f.close() return keys +def agent_auth(username, t, event): + agent = paramiko.Agent() + agent_keys = agent.get_keys() + if len(agent_keys) > 0: + for key in agent_keys: + print 'Trying ssh-agent key %s' % paramiko.util.hexify(key.get_fingerprint()), + t.auth_publickey(username, key, event) + event.wait(10) + if t.is_authenticated(): + print '... success!' + return + print '... nope.' + +def manual_auth(username, hostname, event): + default_auth = 'p' + auth = raw_input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth) + if len(auth) == 0: + auth = default_auth + + if auth == 'r': + default_path = os.environ['HOME'] + '/.ssh/id_rsa' + path = raw_input('RSA key [%s]: ' % default_path) + if len(path) == 0: + path = default_path + try: + key = paramiko.RSAKey.from_private_key_file(path) + except paramiko.PasswordRequiredException: + password = getpass.getpass('RSA key password: ') + key = paramiko.RSAKey.from_private_key_file(path, password) + t.auth_publickey(username, key, event) + elif auth == 'd': + default_path = os.environ['HOME'] + '/.ssh/id_dsa' + path = raw_input('DSS key [%s]: ' % default_path) + if len(path) == 0: + path = default_path + try: + key = paramiko.DSSKey.from_private_key_file(path) + except paramiko.PasswordRequiredException: + password = getpass.getpass('DSS key password: ') + key = paramiko.DSSKey.from_private_key_file(path, password) + t.auth_publickey(username, key, event) + else: + pw = getpass.getpass('Password for %s@%s: ' % (username, hostname)) + t.auth_password(username, pw, event) + ##### main demo @@ -113,37 +158,11 @@ try: if len(username) == 0: username = default_username + agent_auth(username, t, event) + # ask for what kind of authentication to try - default_auth = 'p' - auth = raw_input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth) - if len(auth) == 0: - auth = default_auth - - if auth == 'r': - default_path = os.environ['HOME'] + '/.ssh/id_rsa' - path = raw_input('RSA key [%s]: ' % default_path) - if len(path) == 0: - path = default_path - try: - key = paramiko.RSAKey.from_private_key_file(path) - except paramiko.PasswordRequiredException: - password = getpass.getpass('RSA key password: ') - key = paramiko.RSAKey.from_private_key_file(path, password) - t.auth_publickey(username, key, event) - elif auth == 'd': - default_path = os.environ['HOME'] + '/.ssh/id_dsa' - path = raw_input('DSS key [%s]: ' % default_path) - if len(path) == 0: - path = default_path - try: - key = paramiko.DSSKey.from_private_key_file(path) - except paramiko.PasswordRequiredException: - password = getpass.getpass('DSS key password: ') - key = paramiko.DSSKey.from_private_key_file(path, password) - t.auth_publickey(username, key, event) - else: - pw = getpass.getpass('Password for %s@%s: ' % (username, hostname)) - t.auth_password(username, pw, event) + if not t.is_authenticated(): + manual_auth(username, hostname, event) event.wait(10) # print repr(t) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 0cb8405..1f2b364 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -65,7 +65,7 @@ __license__ = "GNU Lesser General Public License (LGPL)" import transport, auth_transport, channel, rsakey, dsskey, message -import ssh_exception, file, packet +import ssh_exception, file, packet, agent import sftp, sftp_client, sftp_attr, sftp_file, sftp_handle, sftp_server, sftp_si randpool = transport.randpool @@ -89,6 +89,7 @@ SubsystemHandler = server.SubsystemHandler SecurityOptions = transport.SecurityOptions BufferedFile = file.BufferedFile Packetizer = packet.Packetizer +Agent = agent.Agent from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, \ @@ -103,6 +104,7 @@ __all__ = [ 'Transport', 'Channel', 'RSAKey', 'DSSKey', + 'Agent', 'Message', 'SSHException', 'PasswordRequiredException', diff --git a/paramiko/agent.py b/paramiko/agent.py new file mode 100644 index 0000000..7e32c79 --- /dev/null +++ b/paramiko/agent.py @@ -0,0 +1,133 @@ +# Copyright (C) 2003-2005 John Rochester +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +SSH Agent interface for Unix clients. +""" + +import os, socket, struct + +from ssh_exception import SSHException +from message import Message +from pkey import PKey + +SSH2_AGENTC_REQUEST_IDENTITIES, SSH2_AGENT_IDENTITIES_ANSWER, \ + SSH2_AGENTC_SIGN_REQUEST, SSH2_AGENT_SIGN_RESPONSE = range(11, 15) + +class Agent: + """ + Client interface for using private keys from an SSH agent running on the + local machine. If an SSH agent is running, this class can be used to + connect to it and retreive L{PKey} objects which can be used when + attempting to authenticate to remote SSH servers. + + Because the SSH agent protocol uses environment variables and unix-domain + sockets, this probably doesn't work on Windows. It does work on most + posix platforms though (Linux and MacOS X, for example). + """ + + def __init__(self): + """ + Open a session with the local machine's SSH agent, if one is running. + If no agent is running, initialization will succeed, but L{get_keys} + will return an empty tuple. + + @raise SSHException: if an SSH agent is found, but speaks an + incompatible protocol + """ + if 'SSH_AUTH_SOCK' in os.environ: + conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + conn.connect(os.environ['SSH_AUTH_SOCK']) + self.conn = conn + type, result = self._send_message(chr(SSH2_AGENTC_REQUEST_IDENTITIES)) + if type != SSH2_AGENT_IDENTITIES_ANSWER: + raise SSHException('could not get keys from ssh-agent') + keys = [] + for i in range(result.get_int()): + keys.append(AgentKey(self, result.get_string())) + result.get_string() + self.keys = tuple(keys) + else: + self.keys = () + + def close(self): + """ + Close the SSH agent connection. + """ + self.conn.close() + self.conn = None + self.keys = () + + def get_keys(self): + """ + Return the list of keys available through the SSH agent, if any. If + no SSH agent was running (or it couldn't be contacted), an empty list + will be returned. + + @return: a list of keys available on the SSH agent + @rtype: tuple of L{AgentKey} + """ + return self.keys + + def _send_message(self, msg): + msg = str(msg) + self.conn.send(struct.pack('>I', len(msg)) + msg) + l = self._read_all(4) + msg = Message(self._read_all(struct.unpack('>I', l)[0])) + return ord(msg.get_byte()), msg + + def _read_all(self, wanted): + result = self.conn.recv(wanted) + while len(result) < wanted: + if len(result) == 0: + raise SSHException('lost ssh-agent') + extra = self.conn.recv(wanted - len(result)) + if len(extra) == 0: + raise SSHException('lost ssh-agent') + result += extra + return result + + +class AgentKey(PKey): + """ + Private key held in a local SSH agent. This type of key can be used for + authenticating to a remote server (signing). Most other key operations + work as expected. + """ + + def __init__(self, agent, blob): + self.agent = agent + self.blob = blob + self.name = Message(blob).get_string() + + def __str__(self): + return self.blob + + def get_name(self): + return self.name + + def sign_ssh_data(self, randpool, data): + msg = Message() + msg.add_byte(chr(SSH2_AGENTC_SIGN_REQUEST)) + msg.add_string(self.blob) + msg.add_string(data) + msg.add_int(0) + type, result = self.agent._send_message(msg) + if type != SSH2_AGENT_SIGN_RESPONSE: + raise SSHException('key cannot be used for signing') + return result.get_string() diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 50591cb..8a86ec1 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -114,7 +114,7 @@ class SFTPClient (BaseSFTP): raise SFTPError('Expected handle') handle = msg.get_string() filelist = [] - while 1: + while True: try: t, msg = self._request(CMD_READDIR, handle) except EOFError, e: