Skip to content
This repository has been archived by the owner on Sep 12, 2020. It is now read-only.

Commit

Permalink
[project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-18]
Browse files Browse the repository at this point in the history
added public-key support to server mode, more docs
added public-key support to server mode (it can now verify a client signature)
and added a demo of that to the demo_server.py script (user_rsa_key).  in the
process, cleaned up the API of PKey so that now it only has to know about
signing and verifying ssh2 blobs, and can be hashed and compared with other
keys (comparing & hashing only the public parts of the key).  keys can also
be created from strings now too.

some more documentation and hiding private methods.
  • Loading branch information
Robey Pointer committed Dec 30, 2003
1 parent 48c7d88 commit daa8a2e
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 188 deletions.
3 changes: 2 additions & 1 deletion Makefile
Expand Up @@ -8,8 +8,9 @@ RELEASE=charmander
release:
python ./setup.py sdist --formats=zip

docs:
docs: always
epydoc -o docs/ paramiko
always:

# places where the version number is stored:
#
Expand Down
25 changes: 18 additions & 7 deletions demo_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/python

import sys, os, socket, threading, logging, traceback
import sys, os, socket, threading, logging, traceback, base64
import paramiko

# setup logging
Expand All @@ -18,10 +18,14 @@
host_key = paramiko.DSSKey()
host_key.read_private_key_file('demo_dss_key')

print 'Read key: ' + paramiko.hexify(host_key.get_fingerprint())
print 'Read key: ' + paramiko.util.hexify(host_key.get_fingerprint())


class ServerTransport(paramiko.Transport):
# 'data' is the output of base64.encodestring(str(key))
data = 'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hpfAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMCKDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iTUWT10hcuO4Ks8='
good_pub_key = paramiko.RSAKey(data=base64.decodestring(data))

def check_channel_request(self, kind, chanid):
if kind == 'session':
return ServerChannel(chanid)
Expand All @@ -32,6 +36,11 @@ def check_auth_password(self, username, password):
return self.AUTH_SUCCESSFUL
return self.AUTH_FAILED

def check_auth_publickey(self, username, key):
if (username == 'robey') and (key == self.good_pub_key):
return self.AUTH_SUCCESSFUL
return self.AUTH_FAILED

class ServerChannel(paramiko.Channel):
"Channel descendant that pretends to understand pty and shell requests"

Expand Down Expand Up @@ -79,11 +88,13 @@ def check_shell_request(self):
t.add_server_key(host_key)
t.ultra_debug = 0
t.start_server(event)
# print repr(t)
event.wait(10)
if not t.is_active():
print '*** SSH negotiation failed.'
sys.exit(1)
while 1:
event.wait(0.1)
if not t.is_active():
print '*** SSH negotiation failed.'
sys.exit(1)
if event.isSet():
break
# print repr(t)

# wait for auth
Expand Down
2 changes: 1 addition & 1 deletion paramiko/__init__.py
Expand Up @@ -41,5 +41,5 @@ class DSSKey (dsskey.DSSKey):


__all__ = [ 'Transport', 'Channel', 'RSAKey', 'DSSKey', 'transport',
'auth_transport', 'channel', 'rsakey', 'ddskey', 'util',
'auth_transport', 'channel', 'rsakey', 'dsskey', 'util',
'SSHException' ]
189 changes: 143 additions & 46 deletions paramiko/auth_transport.py
Expand Up @@ -13,7 +13,10 @@


class Transport (BaseTransport):
"BaseTransport with the auth framework hooked up"
"""
Subclass of L{BaseTransport} that handles authentication. This separation
keeps either class file from being too unwieldy.
"""

AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3)

Expand Down Expand Up @@ -53,15 +56,23 @@ def is_authenticated(self):
"""
return self.authenticated and self.active

def _request_auth(self):
m = Message()
m.add_byte(chr(_MSG_SERVICE_REQUEST))
m.add_string('ssh-userauth')
self._send_message(m)

def auth_key(self, username, key, event):
"""
Authenticate to the server using a private key. The key is used to
sign data from the server, so it must include the private part. The
given L{event} is triggered on success or failure. On success,
L{is_authenticated} will return C{True}.
@param username: the username to authenticate as.
@type username: string
@param key: the private key to authenticate with.
@type key: L{PKey <pkey.PKey>}
@param event: an event to trigger when the authentication attempt is
complete (whether it was successful or not)
@type event: threading.Event
"""
if (not self.active) or (not self.initial_kex_done):
# we should never try to send the password unless we're on a secure link
# we should never try to authenticate unless we're on a secure link
raise SSHException('No existing session')
try:
self.lock.acquire()
Expand All @@ -74,7 +85,20 @@ def auth_key(self, username, key, event):
self.lock.release()

def auth_password(self, username, password, event):
'authenticate using a password; event is triggered on success or fail'
"""
Authenticate to the server using a password. The username and password
are sent over an encrypted link, and the given L{event} is triggered on
success or failure. On success, L{is_authenticated} will return
C{True}.
@param username: the username to authenticate as.
@type username: string
@param password: the password to authenticate with.
@type password: string
@param event: an event to trigger when the authentication attempt is
complete (whether it was successful or not)
@type event: threading.Event
"""
if (not self.active) or (not self.initial_kex_done):
# we should never try to send the password unless we're on a secure link
raise SSHException('No existing session')
Expand All @@ -88,7 +112,58 @@ def auth_password(self, username, password, event):
finally:
self.lock.release()

def disconnect_service_not_available(self):
def get_allowed_auths(self, username):
"override me!"
return 'password'

def check_auth_none(self, username):
"override me! return int ==> auth status"
return self.AUTH_FAILED

def check_auth_password(self, username, password):
"override me! return int ==> auth status"
return self.AUTH_FAILED

def check_auth_publickey(self, username, key):
"""
I{(subclass override)}
Determine if a given key supplied by the client is acceptable for use
in authentication. You should override this method in server mode to
check the username and key and decide if you would accept a signature
made using this key.
Return C{AUTH_FAILED} if the key is not accepted, C{AUTH_SUCCESSFUL}
if the key is accepted and completes the authentication, or
C{AUTH_PARTIALLY_SUCCESSFUL} if your authentication is stateful, and
this key is accepted for authentication, but more authentication is
required. (In this latter case, L{get_allowed_auths} will be called
to report to the client what options it has for continuing the
authentication.)
The default implementation always returns C{AUTH_FAILED}.
@param username: the username of the authenticating client.
@type username: string
@param key: the key object provided by the client.
@type key: L{PKey <pkey.PKey>}
@return: C{AUTH_FAILED} if the client can't authenticate with this key;
C{AUTH_SUCCESSFUL} if it can; C{AUTH_PARTIALLY_SUCCESSFUL} if it can
authenticate with this key but must continue with authentication.
@rtype: int
"""
return self.AUTH_FAILED


### internals...


def _request_auth(self):
m = Message()
m.add_byte(chr(_MSG_SERVICE_REQUEST))
m.add_string('ssh-userauth')
self._send_message(m)

def _disconnect_service_not_available(self):
m = Message()
m.add_byte(chr(_MSG_DISCONNECT))
m.add_int(_DISCONNECT_SERVICE_NOT_AVAILABLE)
Expand All @@ -97,7 +172,7 @@ def disconnect_service_not_available(self):
self._send_message(m)
self.close()

def disconnect_no_more_auth(self):
def _disconnect_no_more_auth(self):
m = Message()
m.add_byte(chr(_MSG_DISCONNECT))
m.add_int(_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
Expand All @@ -106,7 +181,19 @@ def disconnect_no_more_auth(self):
self._send_message(m)
self.close()

def parse_service_request(self, m):
def _get_session_blob(self, key, service, username):
m = Message()
m.add_string(self.session_id)
m.add_byte(chr(_MSG_USERAUTH_REQUEST))
m.add_string(username)
m.add_string(service)
m.add_string('publickey')
m.add_boolean(1)
m.add_string(key.get_name())
m.add_string(str(key))
return str(m)

def _parse_service_request(self, m):
service = m.get_string()
if self.server_mode and (service == 'ssh-userauth'):
# accepted
Expand All @@ -116,9 +203,9 @@ def parse_service_request(self, m):
self._send_message(m)
return
# dunno this one
self.disconnect_service_not_available()
self._disconnect_service_not_available()

def parse_service_accept(self, m):
def _parse_service_accept(self, m):
service = m.get_string()
if service == 'ssh-userauth':
self._log(DEBUG, 'userauth is OK')
Expand All @@ -134,30 +221,16 @@ def parse_service_accept(self, m):
m.add_boolean(1)
m.add_string(self.private_key.get_name())
m.add_string(str(self.private_key))
m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username))
blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username)
sig = self.private_key.sign_ssh_data(self.randpool, blob)
m.add_string(str(sig))
else:
raise SSHException('Unknown auth method "%s"' % self.auth_method)
self._send_message(m)
else:
self._log(DEBUG, 'Service request "%s" accepted (?)' % service)

def get_allowed_auths(self, username):
"override me!"
return 'password'

def check_auth_none(self, username):
"override me! return int ==> auth status"
return self.AUTH_FAILED

def check_auth_password(self, username, password):
"override me! return int ==> auth status"
return self.AUTH_FAILED

def check_auth_publickey(self, username, key):
"override me! return int ==> auth status"
return self.AUTH_FAILED

def parse_userauth_request(self, m):
def _parse_userauth_request(self, m):
if not self.server_mode:
# er, uh... what?
m = Message()
Expand All @@ -174,11 +247,11 @@ def parse_userauth_request(self, m):
method = m.get_string()
self._log(DEBUG, 'Auth request (type=%s) service=%s, username=%s' % (method, service, username))
if service != 'ssh-connection':
self.disconnect_service_not_available()
self._disconnect_service_not_available()
return
if (self.auth_username is not None) and (self.auth_username != username):
self._log(DEBUG, 'Auth rejected because the client attempted to change username in mid-flight')
self.disconnect_no_more_auth()
self._disconnect_no_more_auth()
return
if method == 'none':
result = self.check_auth_none(username)
Expand All @@ -194,8 +267,32 @@ def parse_userauth_request(self, m):
else:
result = self.check_auth_password(username, password)
elif method == 'publickey':
# FIXME
result = self.check_auth_none(username)
sig_attached = m.get_boolean()
keytype = m.get_string()
keyblob = m.get_string()
key = self._key_from_blob(keytype, keyblob)
if (key is None) or (not key.valid):
self._log(DEBUG, 'Auth rejected: unsupported or mangled public key')
self._disconnect_no_more_auth()
return
# first check if this key is okay... if not, we can skip the verify
result = self.check_auth_publickey(username, key)
if result != self.AUTH_FAILED:
# key is okay, verify it
if not sig_attached:
# client wants to know if this key is acceptable, before it
# signs anything... send special "ok" message
m = Message()
m.add_byte(chr(_MSG_USERAUTH_PK_OK))
m.add_string(keytype)
m.add_string(keyblob)
self._send_message(m)
return
sig = Message(m.get_string())
blob = self._get_session_blob(key, service, username)
if not key.verify_ssh_sig(blob, sig):
self._log(DEBUG, 'Auth rejected: invalid signature')
result = self.AUTH_FAILED
else:
result = self.check_auth_none(username)
# okay, send result
Expand All @@ -215,15 +312,15 @@ def parse_userauth_request(self, m):
self.auth_fail_count += 1
self._send_message(m)
if self.auth_fail_count >= 10:
self.disconnect_no_more_auth()
self._disconnect_no_more_auth()

def parse_userauth_success(self, m):
def _parse_userauth_success(self, m):
self._log(INFO, 'Authentication successful!')
self.authenticated = True
if self.auth_event != None:
self.auth_event.set()

def parse_userauth_failure(self, m):
def _parse_userauth_failure(self, m):
authlist = m.get_list()
partial = m.get_boolean()
if partial:
Expand All @@ -237,19 +334,19 @@ def parse_userauth_failure(self, m):
if self.auth_event != None:
self.auth_event.set()

def parse_userauth_banner(self, m):
def _parse_userauth_banner(self, m):
banner = m.get_string()
lang = m.get_string()
self._log(INFO, 'Auth banner: ' + banner)
# who cares.

_handler_table = BaseTransport._handler_table.copy()
_handler_table.update({
_MSG_SERVICE_REQUEST: parse_service_request,
_MSG_SERVICE_ACCEPT: parse_service_accept,
_MSG_USERAUTH_REQUEST: parse_userauth_request,
_MSG_USERAUTH_SUCCESS: parse_userauth_success,
_MSG_USERAUTH_FAILURE: parse_userauth_failure,
_MSG_USERAUTH_BANNER: parse_userauth_banner,
_MSG_SERVICE_REQUEST: _parse_service_request,
_MSG_SERVICE_ACCEPT: _parse_service_accept,
_MSG_USERAUTH_REQUEST: _parse_userauth_request,
_MSG_USERAUTH_SUCCESS: _parse_userauth_success,
_MSG_USERAUTH_FAILURE: _parse_userauth_failure,
_MSG_USERAUTH_BANNER: _parse_userauth_banner,
})

0 comments on commit daa8a2e

Please sign in to comment.