Commit decc249a authored by Sam Rushing's avatar Sam Rushing

Eric's SSH implementation

parent a856267a
# if y < 1024, openssh will reject us: "bad server public DH value".
# y<1024 means f will be short, and of the form 2^y, so an observer
# could trivially derive our secret y from f. Openssh detects this
# and complains, so avoid creating such values by requiring y to be
# larger than ln2(self.p)
# TODO: we should also look at the value they send to us and reject
# insecure values of f (if g==2 and f has a single '1' bit while the
# rest are '0's, then they must have used a small y also).
# TODO: This could be computed when self.p is set up
# or do as openssh does and scan f for a single '1' bit instead
$Header: //prod/main/ap/ssh/README#1 $
python implementation of SSH2
Eric's Python SSH Library
=========================
Introduction
------------
This is a python implementation of the SSH2 protocol. No effort was made to support SSH1.
It uses Andrew Kuchling's pycrypto library (version 1.9a6).
This implementation is based on the following revisions of the IETF drafts. Future revisions may change certain parts of the protocol, but that is unlikely.
draft-ietf-secsh-architecture-15.txt
draft-ietf-secsh-assignednumbers-05.txt
draft-ietf-secsh-auth-kbdinteract-05.txt
draft-ietf-secsh-connect-18.txt
draft-ietf-secsh-dh-group-exchange-04.txt
draft-ietf-secsh-filexfer-04.txt
draft-ietf-secsh-fingerprint-01.txt
draft-ietf-secsh-gsskeyex-07.txt
draft-ietf-secsh-newmodes-00.txt
draft-ietf-secsh-newmodes-01.txt
draft-ietf-secsh-publickeyfile-04.txt
draft-ietf-secsh-transport-17.txt
draft-ietf-secsh-userauth-18.txt
Overview
--------
This is a very simple overview of the SSH protocol and how it maps to this library's source tree. The IETF secsh architecture document describes the basic architecture of the protocol.
The base-level protocol is called the "transport". You will find its implementation in ssh/transport/transport.py. A subclass of the transport is made to implement either a server or a client (currently only the client is implemented).
The transport is responsible for protocol negotiation, key exchange, encryption, compression, and message authenticity.
The transport may use any type of low-level transmission transports as long as they guarantee in-order delivery. TCP is a perfect example. To support different types of transmission types, the functionality is abstracted in the l4_transport directory (L4 meaning the 4th layer of the OSI network model). You may then use different socket libraries (select vs. poll) or even transmit over other media such as a serial cable (though serial does not offer guaranteed transmission, so it may be a poor choice).
The transport-layer features are abstracted in their respective directories:
cipher - Implements encryption/decryption.
compression - Implements compression.
keys - Formatting and handling of various key types.
key_exchange - The key exchange algorithm (only diffie-hellman).
mac - Message authentication codes.
Services
--------
The SSH transport layer supports different "services". Currently there are two services, "userauth" and "connection". Userauth provides the mechanism to authenticate a user. Connection is the service through which most data transfer is done. On the transport layer you send a message to ask if it is ok to use a service, and if so go ahead.
Userauth
--------
Userauth is a generic mechanism for authentication. It supports various different authentication mechanisms. Currently this library supports publickey and password. Host-based authentication could be trivially added if needed.
Connection
----------
The connection layer is a generic mechanism to have various different "channels". You can multiplex multiple channels over a single connection. The connection layer is also flow-controlled with finite sized windows.
Currently the only channel written is the interactive session channel. It executes the user's shell on the remote end.
Debugging
---------
There is a debugging facility to capture messages and selectively display them to the user. A transport instance has 1 instance of the debug class. The debug class receives messages and determines if the user wants to see them. You can subclass the debug class and change how the information is presented.
Naming Convention
-----------------
Any method that is used to handle incoming packets has the prefix 'msg_'.
All modules and methods are in lowercase_with_underscores.
All classes are in Capitalized_Words_With_Underscores. This is not a standard naming convention, and actually labelled as "ugly!" in the Python style guide. However, I've never liked CapitalizedWords without underscores because it is hard to read. Java style mixedCase has the exact same problem. I like using capitalized words because it is distinguished. I have never done this before, so this is an experiment with this library.
TODO
====
Important
---------
- more docstrings
- Make sure _fastmath and friends are getting installed.
- Add simple_client? See test_client.py.
- Go over every sentence in every spec to make sure this code adheres to it.
- Keys: Add debuging to keys.
- Diffie Hellman: When generating y, make sure that y > ln2(p) (or y > 1024)
- Diffie Hellman: Reject insecure values of f (if g==2 and f has a single '1' bit, then they must have used a small y also)
- Diffie Hellman: Make sure generation of f is good (no single 1 bit)
- CORO: There are problems reading from stdin with coro. test_coro_client doesn't work very well due to this fact.
- connect: I think there are some serious logic problems in the connect code. In some places it manually calls process_messages waiting for a specifc response to a specific message. However, this may cause it to miss other messages that are in the pipeline because there is no registry entry for them. I think to fix the main problem is to change the core process_messages() in the connect class to permanently register its messages.
- finish Remote_To_Local_Forward
- Files in test directory are out of date and broken.
- Update with latest specs (need to see what has changed).
Should Do
---------
- Handle new key exchange (after every gigabyte or every hour)
- More unittests.
- DH Group Exchange
- formalize exceptions with arguments
- exception handling (bad packets, etc.)
- Stir the random pool (see ssh.util.random.py)
- Userauth: Add more debugging.
- Compression support.
- Document how to extend the various components of this library.
- Finish README high-level documentation.
- Test proactive stuff (both successful guess and failures)
- In the userauth module, inspect auths_that_can_continue when getting an authentication failure so that we don't try subsequent authentication methods that we know will not work.
- Figure out what partial_success means in the userauth module.
- Check for invalid channel id's in the connect.py code when looking at self.local_channels.
- Change module definition at top of file to be a triple quoted string instead of a comment. This is the correct docstyle.
- Connect: No way to match channel requests with their response. Need to check latest specs.
Would Be Nice
-------------
- Write a server
- I18N of error messages.
- Handle SSH1.x protocol.
- Userauth: Support pipelining (sending multiple userauth requests without waiting for the server to respond).
- Userauth: Mechanism to remember which auth method succeeds on a per-host basis. Thus in future attempts to connect to the remote host, it can attempt the correct method first.
- BER: Make a complete ASN.1 decoder.
- BER: Break out the BER encodeder into a separate non-SSH specific module.
- BER: Add an encoder.
- Keys: Add key generator.
- Optimize...haven't yet done any performance testing.
Bugs
----
- Fix memory leaks (circular references)
# -*- Mode: Python -*-
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.auth
#
# This handles the various types of authentication systems.
#
import ssh.transport
class Authentication_System(ssh.transport.SSH_Service):
def authenticate(self, service_name):
"""authenticate(self, service_name) -> None
Attempts to authenticate with the remote side.
Assumes you have already confirmed that this authentication service
is OK to use by sending a SSH_MSG_SERVICE_REQUEST packet.
<service_name>: The name of the service that you want to use after
authenticating. Typically 'ssh-connection'.
"""
# XXX: userauth is currently the only defined authentication
# mechanism that I know if. userauth requires to know
# what the service is you are trying to authenticate for.
# This means that this generic API is specialized to include
# service_name only because Userauth is kinda designed weird.
# It is possible that other auth types don't care what service
# you want to run.
raise NotImplementedError
class Authentication_Error(Exception):
pass
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ssh_userauth
#
# This implements the ssh-userauth service for authenticating a user.
#
import os
import ssh.transport
import ssh.auth
import ssh.keys.openssh_key_storage
import ssh.util.packet
import ssh.util
import ssh.util.debug
import ssh.util.password
Authentication_Error = ssh.auth.Authentication_Error
class Userauth_Method_Not_Allowed_Error(Exception):
"""Userauth_Method_Not_Allowed_Error
This is raised when it is determined that the method is not supported.
"""
def __init__(self, auths_that_can_continue):
Exception.__init__(self, auths_that_can_continue)
self.auths_that_can_continue = auths_that_can_continue
class Userauth_Authentication_Method:
"""Userauth_Authentication_Method
This is the base class for various different types of authentication
methods supported by the userauth service.
"""
name = ''
def __init__(self, ssh_transport):
self.ssh_transport = ssh_transport
def authenticate(self, username, service_name):
"""authenticate(self, username, service_name) -> None
Try to authenticate with the remote side.
Raises Authentication_Error if it fails.
"""
raise NotImplementedError
def msg_userauth_failure(self, packet):
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, '%s Auth: Userauth failure.', (self.name,))
msg, auths_that_can_continue, partial_success = ssh.util.packet.unpack_payload(PAYLOAD_MSG_USERAUTH_FAILURE, packet)
# XXX: How to handle partial_success?
if self.name not in auths_that_can_continue:
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, '%s Auth: Not in the list of auths that can continue', (self.name,))
raise Userauth_Method_Not_Allowed_Error(auths_that_can_continue)
class Publickey(Userauth_Authentication_Method):
name = 'publickey'
def authenticate(self, username, service_name):
local_username = os.getlogin()
for key_storage in self.ssh_transport.supported_key_storages:
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Publickey Auth: Trying to load keytype "%s" for user "%s".', (key_storage.__class__.__name__, local_username))
loaded_keys = key_storage.load_keys(username=local_username)
if loaded_keys:
for loaded_key in loaded_keys:
# Test this key type.
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Publickey Auth: Sending PK test for keytype "%s".', (loaded_key.name,))
packet = ssh.util.packet.pack_payload(PAYLOAD_MSG_USERAUTH_REQUEST_PK_TEST,
(SSH_MSG_USERAUTH_REQUEST,
username,
service_name,
'publickey',
0,
loaded_key.name,
loaded_key.get_public_key_blob()
))
self.ssh_transport.send_packet(packet)
message_type, packet = self.ssh_transport.receive_message((SSH_MSG_USERAUTH_PK_OK,
SSH_MSG_USERAUTH_FAILURE,
))
if message_type == SSH_MSG_USERAUTH_PK_OK:
# This public key is ok to try.
try:
self._try_auth(packet, loaded_key, username, service_name)
except Authentication_Error:
# Nope, didn't work. Loop and try next.
pass
else:
# Done!
return
elif message_type == SSH_MSG_USERAUTH_FAILURE:
# Key type not allowed.
self.msg_userauth_failure(packet)
# Loop through and try next key.
else:
# Should never happen.
raise ValueError, message_type
else:
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Publickey Auth: No more key storage types left.')
else:
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Publickey Auth: No keys found of this key storage type.')
else:
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Publickey Auth: No more storage key types left to try.')
raise Authentication_Error
def _try_auth(self, packet, loaded_key, username, service_name):
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Publickey Auth: Got OK for this key type.')
msg, key_algorithm_name, key_blob = ssh.util.packet.unpack_payload(PAYLOAD_MSG_USERAUTH_PK_OK, packet)
assert (key_algorithm_name == loaded_key.name)
# XXX: Check key_blob, too?
# Send the actual request.
# Compute signature.
session_id = self.ssh_transport.key_exchange.session_id
sig_data = ssh.util.packet.pack_payload(PAYLOAD_USERAUTH_REQUEST_PK_SIGNATURE,
(session_id,
SSH_MSG_USERAUTH_REQUEST,
username,
service_name,
'publickey',
1,
loaded_key.name,
loaded_key.get_public_key_blob()
))
signature = loaded_key.sign(sig_data)
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Publickey Auth: Sending userauth request.')
packet = ssh.util.packet.pack_payload(PAYLOAD_MSG_USERAUTH_REQUEST_PK,
(SSH_MSG_USERAUTH_REQUEST,
username,
service_name,
'publickey',
1,
loaded_key.name,
loaded_key.get_public_key_blob(),
signature))
self.ssh_transport.send_packet(packet)
message_type, packet = self.ssh_transport.receive_message((SSH_MSG_USERAUTH_SUCCESS,
SSH_MSG_USERAUTH_FAILURE))
if message_type == SSH_MSG_USERAUTH_SUCCESS:
# Success.
return
elif message_type == SSH_MSG_USERAUTH_FAILURE:
self.msg_userauth_failure(packet)
raise Authentication_Error
else:
# Should never happen.
raise ValueError, message_type
class Password(Userauth_Authentication_Method):
name = 'password'
def authenticate(self, username, service_name):
password = self.get_password(username)
packet = ssh.util.packet.pack_payload(PAYLOAD_MSG_USERAUTH_REQUEST_PASSWORD,
(SSH_MSG_USERAUTH_REQUEST,
username,
service_name,
'password',
0,
password
))
self.ssh_transport.send_packet(packet)
# While loop in case we get a CHANGEREQ packet.
while 1:
try:
message_type, packet = self.ssh_transport.receive_message(( \
SSH_MSG_USERAUTH_SUCCESS,SSH_MSG_USERAUTH_FAILURE,\
SSH_MSG_USERAUTH_PASSWD_CHANGEREQ))
except EOFError:
# In case of an expired user, an EOFError is raised
# Expired accounts are also considered as authentication errors
raise Authentication_Error
if message_type == SSH_MSG_USERAUTH_SUCCESS:
# Success!
return
elif message_type == SSH_MSG_USERAUTH_FAILURE:
self.msg_userauth_failure(packet)
# XXX: Could ask for user's password again?
# XXX: Should handle partial_success flag for CHANGEREQ response.
raise Authentication_Error
elif message_type == SSH_MSG_USERAUTH_PASSWD_CHANGEREQ:
self.msg_userauth_passwd_changereq(packet, username, service_name)
else:
# Should never happen.
raise ValueError, message_type
def get_password(self, username, prompt=None):
if prompt is None:
prompt = '%s\'s password> ' % username
return ssh.util.password.get_password(prompt)
def msg_userauth_passwd_changereq(self, packet, username, service_name):
# User's password has expired. Allow the user to enter a new password.
msg, prompt, language = ssh.util.packet.unpack_payload(PAYLOAD_MSG_USERAUTH_PASSWD_CHANGEREQ, packet)
print ssh.util.safe_string(prompt)
old_password = self.get_password('%s\'s old password> ' % username)
while 1:
new_password = self.get_password('%s\'s new password> ' % username)
new_password2 = self.get_password('Retype new password> ')
if new_password != new_password2:
print 'Passwords did not match! Try again.'
else:
break
packet = ssh.util.packet.pack_payload(PAYLOAD_MSG_USERAUTH_REQUEST_CHANGE_PASSWD,
(SSH_MSG_USERAUTH_REQUEST,
username,
service_name,
'password',
1,
old_password,
new_password))
self.ssh_transport.send_packet(packet)
# Not implemented, yet.
#class Hostbased(Userauth_Authentication_Method):
# name = 'hostbased'
#
# def get_userauth_request(self, username, service_name):
# pass
class Userauth(ssh.auth.Authentication_System):
name = 'ssh-userauth'
methods = None
def __init__(self, ssh_transport):
# Default...You can change.
self.username = os.getlogin()
self.ssh_transport = ssh_transport
# Instantiate methods.
self.methods = [Publickey(ssh_transport), Password(ssh_transport)]
def authenticate(self, service_name):
"""authenticate(self, service_name) -> None
Attempts to authenticate with the remote side.
Assumes you have already confirmed that ssh-userauth is OK to use
by sending a SSH_MSG_SERVICE_REQUEST packet. This will try the
authentication methods listed in self.methods in order until one of
them works.
Raises Authentication_Error if none of the authentication methods work.
<service_name>: The name of the service that you want to use after
authenticating. Typically 'ssh-connection'.
"""
# Assume that all of our auths can continue.
# Userauth_Method_Not_Allowed_Error will update this list if we
# ever receive an error.
auths_that_can_continue = [ method.name for method in self.methods ]
callbacks = {SSH_MSG_USERAUTH_BANNER: self.msg_userauth_banner}
self.ssh_transport.register_callbacks(self.name, callbacks)
try:
for method in self.methods:
if method.name in auths_that_can_continue:
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Trying authentication method "%s".', (method.name,))
try:
method.authenticate(self.username, service_name)
except Authentication_Error:
self.ssh_transport.debug.write(ssh.util.debug.DEBUG_1, 'Authentication method "%s" failed.', (method.name,))
except Userauth_Method_Not_Allowed_Error, why:
auths_that_can_continue = why.auths_that_can_continue
else:
# Authentication success.
return
else:
raise Authentication_Error
finally:
self.ssh_transport.unregister_callbacks(self.name)
def msg_userauth_banner(self, packet):
msg, message, language = ssh.util.packet.unpack_payload(PAYLOAD_MSG_USERAUTH_BANNER, packet)
print ssh.util.safe_string(message)
SSH_MSG_USERAUTH_REQUEST = 50
SSH_MSG_USERAUTH_FAILURE = 51
SSH_MSG_USERAUTH_SUCCESS = 52
SSH_MSG_USERAUTH_BANNER = 53
SSH_MSG_USERAUTH_PK_OK = 60
SSH_MSG_USERAUTH_PASSWD_CHANGEREQ = 60
PAYLOAD_MSG_USERAUTH_FAILURE = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_FAILURE
ssh.util.packet.NAME_LIST, # authentications that can continue
ssh.util.packet.BOOLEAN) # partial success
PAYLOAD_MSG_USERAUTH_SUCCESS = (ssh.util.packet.BYTE,) # SSH_MSG_USERAUTH_SUCCESS
PAYLOAD_MSG_USERAUTH_BANNER = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_BANNER
ssh.util.packet.STRING, # message
ssh.util.packet.STRING) # language tag
PAYLOAD_MSG_USERAUTH_REQUEST_PK_TEST = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_REQUEST
ssh.util.packet.STRING, # username
ssh.util.packet.STRING, # service
ssh.util.packet.STRING, # "publickey"
ssh.util.packet.BOOLEAN, # FALSE
ssh.util.packet.STRING, # public key algorithm name
ssh.util.packet.STRING) # public key blob
PAYLOAD_MSG_USERAUTH_REQUEST_PK = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_REQUEST
ssh.util.packet.STRING, # username
ssh.util.packet.STRING, # service
ssh.util.packet.STRING, # "publickey"
ssh.util.packet.BOOLEAN, # TRUE
ssh.util.packet.STRING, # public key algorithm name
ssh.util.packet.STRING, # public key blob
ssh.util.packet.STRING) # signature
PAYLOAD_USERAUTH_REQUEST_PK_SIGNATURE = (ssh.util.packet.STRING, # session identifier
ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_REQUEST
ssh.util.packet.STRING, # username
ssh.util.packet.STRING, # service
ssh.util.packet.STRING, # "publickey"
ssh.util.packet.BOOLEAN, # TRUE
ssh.util.packet.STRING, # public key algorithm name
ssh.util.packet.STRING) # public key to be used for authentication
PAYLOAD_MSG_USERAUTH_PK_OK = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_PK_OK
ssh.util.packet.STRING, # public key algorithm name from the request
ssh.util.packet.STRING) # public key blob from the request
PAYLOAD_MSG_USERAUTH_REQUEST_PASSWORD = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_REQUEST
ssh.util.packet.STRING, # username
ssh.util.packet.STRING, # service
ssh.util.packet.STRING, # "password"
ssh.util.packet.BOOLEAN,# FALSE
ssh.util.packet.STRING) # plaintext password
PAYLOAD_MSG_USERAUTH_PASSWD_CHANGEREQ = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
ssh.util.packet.STRING, # prompt
ssh.util.packet.STRING) # language tag
PAYLOAD_MSG_USERAUTH_REQUEST_CHANGE_PASSWD = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_REQUEST
ssh.util.packet.STRING, # username
ssh.util.packet.STRING, # service
ssh.util.packet.STRING, # "password"
ssh.util.packet.BOOLEAN, # TRUE
ssh.util.packet.STRING, # plaintext old password
ssh.util.packet.STRING) # plaintext new password
PAYLOAD_MSG_USERAUTH_REQUEST_HOSTBASED = (ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_REQUEST
ssh.util.packet.STRING, # username
ssh.util.packet.STRING, # service
ssh.util.packet.STRING, # "hostbased"
ssh.util.packet.STRING, # public key algorithm for host key
ssh.util.packet.STRING, # public host key and certificates for client host
ssh.util.packet.STRING, # client host name
ssh.util.packet.STRING, # username on the client host
ssh.util.packet.STRING) # signature
PAYLOAD_USERAUTH_REQUEST_HOSTBASED_SIGNATURE = (ssh.util.packet.STRING, # session identifier
ssh.util.packet.BYTE, # SSH_MSG_USERAUTH_REQUEST
ssh.util.packet.STRING, # username
ssh.util.packet.STRING, # service
ssh.util.packet.STRING, # "hostbased"
ssh.util.packet.STRING, # public key algorithm for host key
ssh.util.packet.STRING, # public host key and certificates for client host
ssh.util.packet.STRING, # client host name
ssh.util.packet.STRING) # username on the client host
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.cipher
#
# This is the interface to the encryption/decryption routines.
#
__version__ = '$Revision: #1 $'
class SSH_Cipher_Method:
"""SSH_Cipher_Method
Base class for any type of stream encryption.
"""
name = 'none'
key_size = 0
iv_size = 0
block_size = 1
IV = None
key = None
def encrypt(self, data):
raise NotImplementedError
def decrypt(self, data):
raise NotImplementedError
def set_encryption_key_and_iv(self, key, IV):
self.key = key
self.IV = IV
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.cipher.blowfish_cbc
#
# Implements the Blowfish cipher in CBC mode.
#
import ssh.cipher
from Crypto.Cipher import Blowfish
class Blowfish_CBC(ssh.cipher.SSH_Cipher_Method):
name = 'blowfish-cbc'
block_size = 8
key_size = 16
iv_size = 8
cipher = None
def encrypt(self, data):
return self.cipher.encrypt(data)
def descrypt(self, data):
return self.cipher.decrypt(data)
def set_encryption_key_and_iv(self, key, IV):
self.key = key
self.IV = IV
self.cipher = Blowfish.new(key, Blowfish.MODE_CBC, IV)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.cipher.des3_cbc
#
# Implements the Triple DES cipher in CBC mode.
#
__version__ = '$Revision: #1 $'
import ssh.cipher
from Crypto.Cipher import DES3
class Triple_DES_CBC(ssh.cipher.SSH_Cipher_Method):
name = '3des-cbc'
block_size = 8
key_size = 24
iv_size = 8
cipher = None
def encrypt(self, data):
return self.cipher.encrypt(data)
def decrypt(self, data):
return self.cipher.decrypt(data)
def set_encryption_key_and_iv(self, key, IV):
self.key = key
self.IV = IV
self.cipher = DES3.new(key, DES3.MODE_CBC, IV)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.cipher.none
#
# This is the 'none' cipher. It passes the data through without encryption.
#
__version__ = '$Revision: #1 $'
import ssh.cipher
class Cipher_None(ssh.cipher.SSH_Cipher_Method):
def encrypt(self, data):
return data
def decrypt(self, data):
return data
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.compression
#
# This is the interface to support compression of data over the SSH transport.
#
__version__ = '$Revision: #1 $'
class SSH_Compression_Method:
"""SSH_Compression_Method
Base class for any type of compression.
"""
name = 'none'
def compress(self, data):
raise NotImplementedError
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.compression.none
#
# This is the 'none' compression method.
# It passes the data through uncompressed.
#
__version__ = '$Revision: #1 $'
import ssh.compression
class Compression_None(ssh.compression.SSH_Compression_Method):
def compress(self, data):
return data
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.connection.channel
#
# This is the base class for developing channels
# used by the ssh_connect service.
#
__version__ = '$Revision: #1 $'
import coro
import ssh.connection.data_buffer
import ssh.util.packet
import ssh.util.debug
from ssh.connection.constants import *
class Channel_Request_Failure(Exception):
pass
class Channel_Open_Error(Exception):
def __init__(self, channel_id, reason_code, reason_text, language):
Exception.__init__(self, channel_id, reason_code, reason_text, language)
self.channel_id = channel_id
self.reason_code = reason_code
self.reason_text = reason_text
self.language = language
def __str__(self):
if channel_open_error_strings.has_key(self.reason_code):
reason_code_str = ' (%s)' % channel_open_error_strings[self.reason_code]
else:
reason_code_str = ''
return 'Channel ID %i Open Error: %i%s: %r' % (self.channel_id, self.reason_code, reason_code_str, self.reason_text)
class Channel_Closed_Error(Exception):
pass
# List of channel open error codes.
SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1
SSH_OPEN_CONNECT_FAILED = 2
SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3
SSH_OPEN_RESOURCE_SHORTAGE = 4
channel_open_error_strings = {
SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: 'administratively prohibited',
SSH_OPEN_CONNECT_FAILED: 'connect failed',
SSH_OPEN_UNKNOWN_CHANNEL_TYPE: 'unknown channel type',
SSH_OPEN_RESOURCE_SHORTAGE: 'resource shortage',
}
class Channel:
name = ''
channel_id = 0
window_size = 131072 # 128k
max_packet_size = 131072 # 128k
# Additional ssh.util.packet data types used in the CHANNEL_OPEN message.
additional_packet_data_types = ()
# This is a flag you can change if you want to handle extended data
# differently.
treat_extended_data_as_regular = 1
# This is how many bytes the remote side can send.
# Once it hits zero, I start ignoring data.
# The current algorithm is to increase the window size back to the
# initial size whenever this number drops below one half.
window_data_left = window_size
# Condition variable triggered whenever the window is updated.
window_data_added_cv = None
closed = 1
eof = 1
# This is the instance that is created for data flowing in from the other
# side.
remote_channel = None
# This is a buffer of data received. It is a Buffer instance.
# A value of '' in the buffer indicates EOF.
recv_buffer = None
# This is a buffer of extended data received.
# The key is the data_type_code, and the value is a Buffer instance.
extended_recv_buffer = None
# This is a condition variable triggered when channel request responses
# are received. The thread is awoken with a boolean value that indicates
# whether or not the request succeeded.
# XXX: There is a problem that this doesn't handle concurrent requests.
channel_request_cv = None
# This is a condition variable triggered when open success or failure
# is received.
channel_open_cv = None
def __init__(self, connection_service):
self.connection_service = connection_service
# Local reference for convenience.
self.transport = connection_service.transport
self.recv_buffer = ssh.connection.data_buffer.Buffer()
self.extended_recv_buffer = {}
self.remote_channel = Remote_Channel()
self.window_data_added_cv = coro.condition_variable()
self.channel_request_cv = coro.condition_variable()
self.channel_open_cv = coro.condition_variable()
def __str__(self):
return '<Channel %s ID:%i>' % (self.name, self.channel_id)
def get_additional_open_data(self):
"""get_additional_open_data(self) -> data
Returns the additional information used for opening a channel.
<data> is a tuple of the actual data that is appended to the packet.
"""
# No additional data by default.
return ()
def set_additional_open_data(self, data):
"""set_additional_open_data(self, data) -> None
Sets the additional data information.
<data> is a tuple of the data elements specific to this channel type.
"""
# By default ignore any open data.
return None
def send_channel_request(self, request_type, payload, data, want_reply=1, default_reply_handler=True):
"""send_channel_request(self, request_type, payload, data, want_reply=1, default_reply_handler=True) -> None
This is a generic mechanism for sending a channel request packet.
<request_type>: Request type to send.
<payload>: The ssh_packet format definition.
<data>: The data to go with the payload.
<want_reply>: The want_reply flag.
<default_reply_handler>: If true and want_reply is True, then the
default reply handler will be used. The default reply
handler is pretty simple. Will just return if
CHANNEL_REQUEST_SUCCESS is received. Will raise
Channel_Request_Failure if FAILURE is received.
"""
# XXX: There is a problem with the spec. It does not indicate how to
# match requests with responses. IIRC, they talked about it on
# the mailing list and updated the spec. Need to investigate if
# they have clarified the spec on how to handle this.
# XXX: Or maybe concurrent channel requests on the same channel are
# not allowed?
if self.remote_channel.closed:
raise Channel_Closed_Error
packet = ssh.util.packet.pack_payload(SSH_MSG_CHANNEL_REQUEST_PAYLOAD,
(SSH_MSG_CHANNEL_REQUEST,
self.remote_channel.channel_id,
request_type,
int(want_reply)))
packet_data = ssh.util.packet.pack_payload(payload, data)
self.transport.send_packet(packet + packet_data)
if want_reply and default_reply_handler:
# Wait for response.
assert len(self.channel_request_cv)==0, 'Concurrent channel requests not supported!'
if not self.channel_request_cv.wait():
raise Channel_Request_Failure
def append_data_received(self, data):
"""append_data_received(self, data) -> None
Indicates that the given data was received.
"""
if data:
self.recv_buffer.write(data)
def append_extended_data_received(self, data_type_code, data):
"""append_extended_data_received(self, data_type_code, data) -> None
Indicates that the given extended data was received.
"""
if data:
if self.treat_extended_data_as_regular:
self.append_data_received(data)
else:
if self.extended_recv_buffer.has_key(data_type_code):
self.extended_recv_buffer[data_type_code].write(data)
else:
b = ssh.connection.data_buffer.Buffer()
b.write(data)
self.extended_recv_buffer[data_type_code] = b
def set_eof(self):
"""set_eof(self) -> None
Indicate that there is no more data on this channel.
"""
self.eof = 1
self.recv_buffer.write('')
for b in self.extended_recv_buffer.values():
b.write('')
def handle_request(self, request_type, want_reply, type_specific_packet_data):
# Default is always to fail. Specific channel types override this method.
if want_reply:
packet = ssh.util.packet.pack_payload(SSH_MSG_CHANNEL_FAILURE_PAYLOAD,
(SSH_MSG_CHANNEL_FAILURE,
self.remote_channel.channel_id,))
self.transport.send_packet(packet)
def open(self):
"""open(self) -> None
Opens the channel to the remote side.
"""
assert self.closed
self.connection_service.register_channel(self)
self.transport.debug.write(ssh.util.debug.DEBUG_2, 'sending channel open request channel ID %i', (self.channel_id,))
# Send the open request.
additional_data = self.get_additional_open_data()
packet_payload = SSH_MSG_CHANNEL_OPEN_PAYLOAD + self.additional_packet_data_types
packet_data = (SSH_MSG_CHANNEL_OPEN,
self.name,
self.channel_id,
self.window_size,
self.max_packet_size) + additional_data
packet = ssh.util.packet.pack_payload(packet_payload, packet_data)
self.transport.send_packet(packet)
success, data = self.channel_open_cv.wait()
if success:
self.set_additional_open_data(data)
else:
reason_code, reason_text, language = data
raise Channel_Open_Error(self.channel_id, reason_code, reason_text, language)
def close(self):
"""close(self) -> None
Tell remote side to close its channel.
Our side is not considered "closed" until after we receive
SSH_MSG_CHANNEL_CLOSE from the remote side.
"""
if not self.remote_channel.closed:
self.remote_channel.closed = 1
packet = ssh.util.packet.pack_payload(SSH_MSG_CHANNEL_CLOSE_PAYLOAD,
(SSH_MSG_CHANNEL_CLOSE,
self.remote_channel.channel_id))
self.transport.send_packet(packet)
# We need to cause any threads that were trying to write on
# this channel to stop trying to write. If they were asleep
# waiting for one of the three condition variables, we need to
# wake them up. They will notice that self.remote_channel.closed
# is now true, and will do the right thing.
self.window_data_added_cv.wake_all()
self.channel_request_cv.wake_all(False)
self.channel_open_cv.wake_all((False, (SSH_OPEN_CONNECT_FAILED,
'Channel has been closed',
None)))
def send_window_adjustment(self, bytes_to_add):
self.transport.debug.write(ssh.util.debug.DEBUG_2, 'sending window adjustment to add %i bytes', (bytes_to_add,))
packet = ssh.util.packet.pack_payload(SSH_MSG_CHANNEL_WINDOW_ADJUST_PAYLOAD,
(SSH_MSG_CHANNEL_WINDOW_ADJUST,
self.remote_channel.channel_id,
bytes_to_add))
self.transport.send_packet(packet)
self.window_data_left += bytes_to_add
def has_data_to_read(self, extended=None):
"""has_data_to_read(self, extended=None) -> boolean
Returns whether or not there is data available to read.
<extended>: data_type_code of extended data type to read.
Set to None for normal data.
"""
if extended is None:
b = self.recv_buffer
else:
if self.extended_recv_buffer.has_key(extended):
b = self.extended_recv_buffer[extended]
else:
return False
if b and b.fifo.peek() != '':
return True
else:
return False
def _check_window_adjust(self):
if self.window_data_left < self.window_size/2:
# Increase the window so that the other side may send more data.
self.send_window_adjustment(self.window_size - self.window_data_left)
def read(self, bytes, extended=None):
"""read(self, bytes, extended=None) -> data
Read data off the channel.
Reads at most <bytes> bytes. It may return less than <bytes> even
if there is more data in the buffer.
<bytes>: Number of bytes to read.
<extended>: data_type_code of extended data type to read.
Set to None to read normal data.
"""
if extended is not None:
if not self.extended_recv_buffer.has_key(extended):
self.extended_recv_buffer[extended] = ssh.connection.data_buffer.Buffer()
b = self.extended_recv_buffer[extended]
else:
b = self.recv_buffer
result = b.read_at_most(bytes)
# Only adjust the window when the buffer is clear.
if not b:
self._check_window_adjust()
return result
def read_exact(self, bytes, extended=None):
"""read_exact(self, bytes, extended=None) -> data
Read exactly <bytes> number of bytes off the channel.
May return less than <bytes> bytes if EOF is reached.
<bytes>: Number of bytes to read. Blocks until enough data is
available.
<extended>: data_type_code of extended data type to read.
Set to None to read normal data.
"""
if extended is not None:
if not self.extended_recv_buffer.has_key(extended):
self.extended_recv_buffer[extended] = ssh.connection.data_buffer.Buffer()
b = self.extended_recv_buffer[extended]
else:
b = self.recv_buffer
result = []
bytes_left = bytes
while bytes_left > 0:
data = b.read_at_most(bytes_left)
if not data:
if result:
return ''.join(result)
else:
raise EOFError
result.append(data)
bytes_left -= len(data)
# Only adjust the window when the buffer is clear.
if not b:
self._check_window_adjust()
return ''.join(result)
# Make an alias for convenience.
recv = read
def send(self, data):
"""send(self, data) -> None
Send the given data string.
"""
data_start = 0
while data_start < len(data):
if self.remote_channel.closed:
raise Channel_Closed_Error
while self.remote_channel.window_data_left==0:
# Currently waiting for window update.
self.window_data_added_cv.wait()
# check again inside loop since if we're closed, the window
# might never update
if self.remote_channel.closed:
raise Channel_Closed_Error
# Send what we can.
max_size = min(self.remote_channel.window_data_left, self.remote_channel.max_packet_size)
data_to_send = data[data_start:data_start+max_size]
data_start += max_size
packet = ssh.util.packet.pack_payload(SSH_MSG_CHANNEL_DATA_PAYLOAD,
(SSH_MSG_CHANNEL_DATA,
self.remote_channel.channel_id,
data_to_send))
self.transport.debug.write(ssh.util.debug.DEBUG_3, 'channel %i window lowered by %i to %i', (self.remote_channel.channel_id, len(data_to_send), self.remote_channel.window_data_left))
self.remote_channel.window_data_left -= len(data_to_send)
self.transport.send_packet(packet)
def send_extended(self, data, data_type_code):
"""send_extended(self, data, data_type_code) -> None
Send the given data string as extended data with the given data_type_code.
"""
data_start = 0
while data_start < len(data):
if self.remote_channel.closed:
raise Channel_Closed_Error
while self.remote_channel.window_data_left==0:
# Currently waiting for window update.
self.window_data_added_cv.wait()
# check again inside loop since if we're closed, the window
# might never update
if self.remote_channel.closed:
raise Channel_Closed_Error
# Send what we can.
max_size = min(self.remote_channel.window_data_left, self.remote_channel.max_packet_size)
data_to_send = data[data_start:data_start+max_size]
data_start += max_size
packet = ssh.util.packet.pack_payload(SSH_MSG_CHANNEL_EXTENDED_DATA_PAYLOAD,
(SSH_MSG_CHANNEL_EXTENDED_DATA,
self.remote_channel.channel_id,
data_type_code,
data_to_send))
self.remote_channel.window_data_left -= len(data_to_send)
self.transport.send_packet(packet)
def channel_request_success(self):
"""channel_request_success(self) -> None
This is called whenever a CHANNEL_SUCCESS message is received.
"""
self.channel_request_cv.wake_one(args=True)
def channel_request_failure(self):
"""channel_request_success(self) -> None
This is called whenever a CHANNEL_FAILURE message is received.
"""
self.channel_request_cv.wake_one(args=False)
def channel_open_success(self, data):
"""channel_open_success(self, data) -> None
Indicates the channel is opened.
<data> is a tuple of the data elements specific to this channel type.
"""
# Default is to ignore any extra data.
assert len(self.channel_open_cv)==1
self.channel_open_cv.wake_one((True, data))
def channel_open_failure(self, reason_code, reason_text, language):
"""channel_open_failure(self, reason_code, reason_text, language) -> None
This is called when opening a channel fails.
"""
assert len(self.channel_open_cv)==1
self.channel_open_cv.wake_one((False, (reason_code, reason_text, language)))
class Remote_Channel:
channel_id = 0
window_size = 131072 # 128k
max_packet_size = 131072 # 128k
# This is how many bytes I can send to the remote side.
# Once it hits zero, I start buffering data.
window_data_left = window_size
closed = 1
eof = 1
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.connection.connect
#
# This implements the SSH Connect service. This service can run interactive
# login sessions, remote execution of comments, forwarded TCP/IP connections,
# and forwarded X11 connections. These channels can be multiplexed into a
# single encrypted tunnel.
#
__version__ = '$Revision: #1 $'
import ssh.transport
import ssh.util.packet
import ssh.util.debug
from constants import *
class Connection_Service(ssh.transport.SSH_Service):
name = 'ssh-connection'
# This is a counter used to assign local channel ID's.
next_channel_id = 0
# This is a dictionary of channels.
# The key is the channel ID, the value is the Channel object.
local_channels = None
# This is a dictionary of remote channels.
# The key is the remote channel ID, the value is a Remote_Channel object.
remote_channels = None
def __init__(self, transport):
self.transport = transport
self.local_channels = {}
self.remote_channels = {}
callbacks = {SSH_MSG_GLOBAL_REQUEST: self.msg_global_request,
SSH_MSG_CHANNEL_WINDOW_ADJUST: self.msg_channel_window_adjust,
SSH_MSG_CHANNEL_DATA: self.msg_channel_data,
SSH_MSG_CHANNEL_EXTENDED_DATA: self.msg_channel_extended_data,
SSH_MSG_CHANNEL_EOF: self.msg_channel_eof,
SSH_MSG_CHANNEL_CLOSE: self.msg_channel_close,
SSH_MSG_CHANNEL_REQUEST: self.msg_channel_request,
SSH_MSG_CHANNEL_SUCCESS: self.msg_channel_success,
SSH_MSG_CHANNEL_FAILURE: self.msg_channel_failure,
SSH_MSG_CHANNEL_OPEN_CONFIRMATION: self.msg_channel_open_confirmation,
SSH_MSG_CHANNEL_OPEN_FAILURE: self.msg_channel_open_failure,
}
self.transport.register_callbacks('ssh-connection', callbacks)
def register_channel(self, channel):
"""register_channel(self, channel) -> None
When opening a channel, this function is called to add it to the
local_channels dictionary and to set the channel id.
"""
channel.channel_id = self.next_channel_id
assert not self.local_channels.has_key(channel.channel_id)
self.next_channel_id += 1 # XXX: Overflow?
self.local_channels[channel.channel_id] = channel
def msg_global_request(self, packet):
# XXX: finish this (it's a server thing)
data, offset = ssh.util.packet.unpack_payload_get_offset(SSH_MSG_GLOBAL_REQUEST_PAYLOAD, packet)
msg, request_name, want_reply = data
raise NotImplementedError
def msg_channel_window_adjust(self, packet):
msg, channel_id, bytes_to_add = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_WINDOW_ADJUST_PAYLOAD, packet)
channel = self.local_channels[channel_id]
channel.remote_channel.window_data_left += bytes_to_add
self.transport.debug.write(ssh.util.debug.DEBUG_3, 'channel %i window increased by %i to %i', (channel.remote_channel.channel_id, bytes_to_add, channel.remote_channel.window_data_left))
channel.window_data_added_cv.wake_all()
def msg_channel_data(self, packet):
msg, channel_id, data = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_DATA_PAYLOAD, packet)
channel = self.local_channels[channel_id]
# XXX: In theory, we should verify that len(data) <= channel.max_packet_size
if len(data) > channel.window_data_left:
self.transport.debug.write(ssh.util.debug.WARNING, 'channel %i %i bytes overflowed window of %i', (channel.channel_id, len(data), channel.remote_channel.window_data_left))
# Data is ignored.
else:
channel.window_data_left -= len(data)
channel.append_data_received(data)
def msg_channel_extended_data(self, packet):
msg, channel_id, data_type_code, data = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_EXTENDED_DATA_PAYLOAD, packet)
channel = self.local_channels[channel_id]
if len(data) > channel.window_data_left:
self.transport.debug.write(ssh.util.debug.WARNING, 'channel %i %i bytes overflowed window of %i', (channel.channel_id, len(data), channel.remote_channel.window_data_left))
# Data is ignored.
else:
channel.window_data_left -= len(data)
channel.append_extended_data_received(data_type_code, data)
def msg_channel_eof(self, packet):
msg, channel_id = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_EOF_PAYLOAD, packet)
channel = self.local_channels[channel_id]
# assert it is not already closed?
channel.set_eof()
def msg_channel_close(self, packet):
msg, channel_id = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_CLOSE_PAYLOAD, packet)
channel = self.local_channels[channel_id]
del self.local_channels[channel_id]
del self.remote_channels[channel.remote_channel.channel_id]
# assert it is not already closed?
channel.closed = 1
if not channel.remote_channel.closed:
# Close the other side.
channel.close()
channel.set_eof()
def msg_channel_request(self, packet):
data, offset = ssh.util.packet.unpack_payload_get_offset(SSH_MSG_CHANNEL_REQUEST_PAYLOAD, packet)
msg, channel_id, request_type, want_reply = data
channel = self.local_channels[channel_id]
channel.handle_request(request_type, want_reply, packet[offset:])
def msg_channel_success(self, packet):
msg, channel_id = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_SUCCESS_PAYLOAD, packet)
channel = self.local_channels[channel_id]
channel.channel_request_success()
def msg_channel_failure(self, packet):
msg, channel_id = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_FAILURE_PAYLOAD, packet)
channel = self.local_channels[channel_id]
channel.channel_request_failure()
def msg_channel_open_confirmation(self, packet):
data, offset = ssh.util.packet.unpack_payload_get_offset(SSH_MSG_CHANNEL_OPEN_CONFIRMATION_PAYLOAD, packet)
msg, recipient_channel, sender_channel, window_size, max_packet_size = data
self.transport.debug.write(ssh.util.debug.DEBUG_1, 'channel %i open confirmation sender_channel=%i window_size=%i max_packet_size=%i', (recipient_channel, sender_channel, window_size, max_packet_size))
channel = self.local_channels[recipient_channel]
# XXX: Assert that the channel is not already open?
channel.closed = 0
channel.eof = 0
channel.remote_channel.closed = 0
channel.remote_channel.channel_id = sender_channel
assert not self.remote_channels.has_key(sender_channel)
self.remote_channels[sender_channel] = channel.remote_channel
channel.remote_channel.window_size = window_size
channel.remote_channel.window_data_left = window_size
channel.remote_channel.max_packet_size = max_packet_size
additional_data = ssh.util.packet.unpack_payload(channel.additional_packet_data_types, packet, offset)
channel.channel_open_success(additional_data)
def msg_channel_open_failure(self, packet):
msg, channel_id, reason_code, reason_text, language = ssh.util.packet.unpack_payload(SSH_MSG_CHANNEL_OPEN_FAILURE_PAYLOAD, packet)
channel = self.local_channels[channel_id]
# XXX: Assert that the channel is not already open?
channel.channel_open_failure(reason_code, reason_text, language)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.connect.constants
#
# Constants used in the connect protocol.
__version__ = '$Revision: #1 $'
import ssh.util.packet
SSH_MSG_GLOBAL_REQUEST = 80
SSH_MSG_GLOBAL_REQUEST_SUCCESS = 81
SSH_MSG_GLOBAL_REQUEST_FAILURE = 82
SSH_MSG_CHANNEL_OPEN = 90
SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91
SSH_MSG_CHANNEL_OPEN_FAILURE = 92
SSH_MSG_CHANNEL_WINDOW_ADJUST = 93
SSH_MSG_CHANNEL_DATA = 94
SSH_MSG_CHANNEL_EXTENDED_DATA = 95
SSH_MSG_CHANNEL_EOF = 96
SSH_MSG_CHANNEL_CLOSE = 97
SSH_MSG_CHANNEL_REQUEST = 98
SSH_MSG_CHANNEL_SUCCESS = 99
SSH_MSG_CHANNEL_FAILURE = 100
# This is the basic open payload. Different session types may add
# additional information to this.
SSH_MSG_CHANNEL_OPEN_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_OPEN
ssh.util.packet.STRING, # channel type
ssh.util.packet.UINT32, # sender channel
ssh.util.packet.UINT32, # initial window size
ssh.util.packet.UINT32) # maximum packet size
# This is the basic confirmation payload. Different session types may add
# addition information to this.
SSH_MSG_CHANNEL_OPEN_CONFIRMATION_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_OPEN_CONFIRMATION,
ssh.util.packet.UINT32, # recipient channel
ssh.util.packet.UINT32, # sender channel
ssh.util.packet.UINT32, # initial window size
ssh.util.packet.UINT32) # maximum packet size
SSH_MSG_CHANNEL_OPEN_FAILURE_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_OPEN_FAILURE
ssh.util.packet.UINT32, # recipient channel
ssh.util.packet.UINT32, # reason_code
ssh.util.packet.STRING, # reason_text
ssh.util.packet.STRING) # language
SSH_MSG_CHANNEL_CLOSE_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_CLOSE
ssh.util.packet.UINT32) # recipient_channel
# This may contain additional request-specific data.
SSH_MSG_GLOBAL_REQUEST_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_GLOBAL_REQUEST
ssh.util.packet.STRING, # request name
ssh.util.packet.BOOLEAN) # want reply
# This may contain additional request-specific data.
SSH_MSG_GLOBAL_REQUEST_SUCCESS_PAYLOAD = (ssh.util.packet.BYTE) # SSH_MSG_GLOBAL_REQUEST_SUCCESS
SSH_MSG_GLOBAL_REQUEST_FAILURE_PAYLOAD = (ssh.util.packet.BYTE) # SSH_MSG_GLOBAL_REQUEST_FAILURE
SSH_MSG_CHANNEL_WINDOW_ADJUST_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_WINDOW_ADJUST
ssh.util.packet.UINT32, # recipient channel
ssh.util.packet.UINT32) # bytes to add
SSH_MSG_CHANNEL_DATA_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_DATA
ssh.util.packet.UINT32, # recipient channel
ssh.util.packet.STRING) # data
SSH_MSG_CHANNEL_EXTENDED_DATA_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_EXTENDED_DATA
ssh.util.packet.UINT32, # recipient channel
ssh.util.packet.UINT32, # data_type_code
ssh.util.packet.STRING) # data
SSH_EXTENDED_DATA_STDERR = 1
SSH_MSG_CHANNEL_EOF_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_EOF
ssh.util.packet.UINT32) # recipient channel
# This may contain addition request-specific data.
SSH_MSG_CHANNEL_REQUEST_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_REQUEST
ssh.util.packet.UINT32, # recipient_channel
ssh.util.packet.STRING, # request type
ssh.util.packet.BOOLEAN) # want reply
SSH_MSG_CHANNEL_FAILURE_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_FAILURE
ssh.util.packet.UINT32) # recipient_channel
SSH_MSG_CHANNEL_SUCCESS_PAYLOAD = (ssh.util.packet.BYTE, # SSH_MSG_CHANNEL_SUCCESS
ssh.util.packet.UINT32) # recipient_channel
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.connection.data_buffer
#
# This implements a simple buffer class that works like a FIFO.
# It is time-effecient since it tries to avoid string copies whenever
# possible.
__version__ = '$Revision: #1 $'
import coro_fifo
class Buffer:
def __init__(self):
self.fifo = coro_fifo.circular_fifo()
def __len__(self):
return len(self.fifo)
def write(self, data):
"""write(self, data) -> None
Writes data to the buffer.
"""
self.fifo.enqueue(data)
def pop(self):
"""pop(self) -> str
Pops the first string from the buffer.
"""
return self.fifo.dequeue()
def read_at_most(self, bytes):
"""read_at_most(self, bytes) -> str
Reads at most <bytes>.
May return less than <bytes> even if there is more data in the buffer.
Returns the empty string when the buffer is empty.
"""
while 1:
try:
data = self.fifo.peek()
except IndexError:
# Buffer empty.
self.fifo.cv.wait()
else:
break
if not data:
raise EOFError
if len(data) > bytes:
result = data[:bytes]
self.fifo.poke(data[bytes:])
return result
else:
return self.fifo.dequeue()
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.connect.interactive_session
#
# This implements the "session" channel of the ssh_connect service.
#
__version__ = '$Revision: #1 $'
import channel
import ssh.util.packet
from connect import *
class Interactive_Session(channel.Channel):
name = 'session'
def send_environment_variable(self, name, value):
self.send_channel_request('env', ENV_CHANNEL_REQUEST_PAYLOAD,
(name,
value))
class Interactive_Session_Client(Interactive_Session):
def open_pty(self, term='', width_char=0, height_char=0, width_pixels=0, height_pixels=0, modes=''):
self.send_channel_request('pty-req', PTY_CHANNEL_REQUEST_PAYLOAD,
(term,
width_char,
height_char,
width_pixels,
height_pixels,
modes))
def open_shell(self):
self.send_channel_request('shell', (), ())
def exec_command(self, command):
self.send_channel_request('exec', EXEC_CHANNEL_REQUEST_PAYLOAD, (command,))
class Interactive_Session_Server(Interactive_Session):
def handle_request(self, request_type, want_reply, type_specific_packet_data):
if self.request_handlers.has_key(request_type):
f = self.request_handlers[request_type]
f(want_reply, type_specific_packet_data)
else:
if want_reply:
packet = ssh.util.packet.pack_payload(SSH_MSG_CHANNEL_FAILURE_PAYLOAD, (self.remote_channel.channel_id,))
self.transport.send_packet(packet)
def handle_pty_request(self, want_reply, type_specific_packet_data):
term, width_char, height_char, width_pixels, height_pixels, modes = ssh.util.packet.unpack_payload(PTY_CHANNEL_REQUEST_PAYLOAD, type_specific_packet_data)
# XXX: NOT FINISHED
def handle_x11_request(self, want_reply, type_specific_packet_data):
single_connection, auth_protocol, auth_cookie, screen_number = ssh.util.packet.unpack_payload(X11_CHANNEL_REQUEST_PAYLOAD, type_specific_packet_data)
# XXX: NOT FINISHED
request_handlers = {'pty-req': handle_pty_request,
'x11-req': handle_x11_request,
}
PTY_CHANNEL_REQUEST_PAYLOAD = (ssh.util.packet.STRING, # TERM environment variable value (e.g., vt100)
ssh.util.packet.UINT32, # terminal width, characters (e.g., 80)
ssh.util.packet.UINT32, # terminal height, rows (e.g., 24)
ssh.util.packet.UINT32, # terminal width, pixels (e.g., 640)
ssh.util.packet.UINT32, # terminal height, pixels (e.g., 480)
ssh.util.packet.STRING) # encoded terminal modes
X11_CHANNEL_REQUEST_PAYLOAD = (ssh.util.packet.BOOLEAN, # single connection
ssh.util.packet.STRING, # x11 authentication protocol
ssh.util.packet.STRING, # x11 authentication cookie
ssh.util.packet.UINT32) # x11 screen number
ENV_CHANNEL_REQUEST_PAYLOAD = (ssh.util.packet.STRING, # variable name
ssh.util.packet.STRING) # variable value
EXEC_CHANNEL_REQUEST_PAYLOAD = (ssh.util.packet.STRING,) # command
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_tty_modes
#
# List of TTY modes used for SSH Interactive Sessions.
#
__version__ = '$Revision: #1 $'
import struct
class Term_Mode_Builder:
def __init__(self):
self.ops = []
def set_mode(self, opcode, value):
self.ops.append(chr(opcode) + struct.pack('>I', value))
def get_mode(self):
return ''.join(self.ops)
TTY_OP_END = 0 # Indicates end of options.
VINTR = 1 # Interrupt character; 255 if none. Similarly for the
# other characters. Not all of these characters are
# supported on all systems.
VQUIT = 2 # The quit character (sends SIGQUIT signal on POSIX
# systems).
VERASE = 3 # Erase the character to left of the cursor.
VKILL = 4 # Kill the current input line.
VEOF = 5 # End-of-file character (sends EOF from the terminal).
VEOL = 6 # End-of-line character in addition to carriage return
# and/or linefeed.
VEOL2 = 7 # Additional end-of-line character.
VSTART = 8 # Continues paused output (normally control-Q).
VSTOP = 9 # Pauses output (normally control-S).
VSUSP = 10 # Suspends the current program.
VDSUSP = 11 # Another suspend character.
VREPRINT = 12 # Reprints the current input line.
VWERASE = 13 # Erases a word left of cursor.
VLNEXT = 14 # Enter the next character typed literally, even if it
# is a special character
VFLUSH = 15 # Character to flush output.
VSWTCH = 16 # Switch to a different shell layer.
VSTATUS = 17 # Prints system status line (load, command, pid etc).
VDISCARD = 18 # Toggles the flushing of terminal output.
IGNPAR = 30 # The ignore parity flag. The parameter SHOULD be 0 if
# this flag is FALSE set, and 1 if it is TRUE.
PARMRK = 31 # Mark parity and framing errors.
INPCK = 32 # Enable checking of parity errors.
ISTRIP = 33 # Strip 8th bit off characters.
INLCR = 34 # Map NL into CR on input.
IGNCR = 35 # Ignore CR on input.
ICRNL = 36 # Map CR to NL on input.
IUCLC = 37 # Translate uppercase characters to lowercase.
IXON = 38 # Enable output flow control.
IXANY = 39 # Any char will restart after stop.
IXOFF = 40 # Enable input flow control.
IMAXBEL = 41 # Ring bell on input queue full.
ISIG = 50 # Enable signals INTR, QUIT, [D]SUSP.
ICANON = 51 # Canonicalize input lines.
XCASE = 52 # Enable input and output of uppercase characters by
# preceding their lowercase equivalents with `\'.
ECHO = 53 # Enable echoing.
ECHOE = 54 # Visually erase chars.
ECHOK = 55 # Kill character discards current line.
ECHONL = 56 # Echo NL even if ECHO is off.
NOFLSH = 57 # Don't flush after interrupt.
TOSTOP = 58 # Stop background jobs from output.
IEXTEN = 59 # Enable extensions.
ECHOCTL = 60 # Echo control characters as ^(Char).
ECHOKE = 61 # Visual erase for line kill.
PENDIN = 62 # Retype pending input.
OPOST = 70 # Enable output processing.
OLCUC = 71 # Convert lowercase to uppercase.
ONLCR = 72 # Map NL to CR-NL.
OCRNL = 73 # Translate carriage return to newline (output).
ONOCR = 74 # Translate newline to carriage return-newline
ONLRET = 75 # Newline performs a carriage return (output).
CS7 = 90 # 7 bit mode.
CS8 = 91 # 8 bit mode.
PARENB = 92 # Parity enable.
PARODD = 93 # Odd parity, else even.
TTY_OP_ISPEED = 128 # Specifies the input baud rate in bits per second.
TTY_OP_OSPEED = 129 # Specifies the output baud rate in bits per second.
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.key_exchange
#
# This is the generic key exchange system.
# Currently the SSH spec only defines one key exchange method (diffie hellman),
# but in theory you could create your own method.
#
__version__ = '$Revision: #1 $'
import ssh.util.packet
import ssh.util.debug
class SSH_Key_Exchange:
"""SSH_Key_Exchange
Base class for any type of key exchange.
"""
name = 'none'
# What type of host key features this kex algorithm wants.
wants_signature_host_key = 0
wants_encryption_host_key = 0
shared_secret = None
exchange_hash = None
# session_id is the session identifier for this connection.
# It is the value of the very first exchange_hash. If new keys are
# exchanged, this will stay the same.
session_id = None
c2s_version_string = ''
s2c_version_string = ''
s2c_kexinit_packet = ''
c2s_kexinit_packet = ''
# The SSH_Transport.
transport = None
def __init__(self, transport):
self.supported_server_keys = []
self.transport = transport
def get_initial_client_kex_packet(self):
"""get_initial_client_kex_packet(self) -> packet
Get the very first packet of the key exchange to be sent by the client.
If the key exchange algorithm does not support the client sending
the first packet, then this function should return None.
"""
raise NotImplementedError
def get_initial_server_kex_packet(self):
"""get_initial_server_kex_packet(self) -> packet
Get the very first packet of the key exchange to be sent by the server.
If the key exchange algorithm does not support the server sending
the first packet, then this function should return None.
"""
raise NotImplementedError
def register_client_callbacks(self):
"""register_client_callbacks(self) -> None
Register callbacks necessary to handle the client side.
"""
raise NotImplementedError
def register_server_callbacks(self):
"""register_server_callbacks(self) -> None
Register callbacks necessary to handle the server side.
"""
raise NotImplementedError
def get_hash_object(self, *args):
"""get_hash_object(self, *args) -> hash_object
This returns a hash object.
This object must have the same API as the sha and md5 modules in
standard python:
- update(str)
- digest()
- hexdigest()
- copy()
Additional args are added to the hash object via update().
"""
hash_object = self._get_hash_object()
for arg in args:
hash_object.update(arg)
return hash_object
def _get_hash_object(self):
"""_get_hash_object(self) -> hash_object
Return a raw hash object (see get_hash_object).
"""
raise NotImplementedError
def get_encryption_key(self, letter, required_size):
"""get_encryption_key(self, letter, required_size) -> key
Computes an encryption key with the given letter.
<required_size> is the length of the key that you require (in bytes).
"""
shared_secret = ssh.util.packet.pack_payload((ssh.util.packet.MPINT,), (self.shared_secret,))
key = self.get_hash_object(
shared_secret,
self.exchange_hash,
letter,
self.session_id).digest()
if len(key) > required_size:
# Key is too big...return only what is needed.
key = key[:required_size]
elif len(key) < required_size:
# Key is not big enough...compute additional hashes until big enough.
# K1 = HASH(K || H || X || session_id) (X is e.g. "A")
# K2 = HASH(K || H || K1)
# K3 = HASH(K || H || K1 || K2)
# ...
# key = K1 || K2 || K3 || ...
self.transport.debug.write(ssh.util.debug.DEBUG_2, 'get_encryption_key: computed key is too small len(key)=%i required_size=%i', (len(key), required_size))
key_data = [key]
key_data_len = len(key)
while key_data_len < required_size:
additional_key_data = self.get_hash_object(shared_secret, self.exchange_hash, ''.join(key_data)).digest()
key_data.append(additional_key_data)
key_data_len += len(additional_key_data)
key = ''.join(key_data)[:required_size]
else:
# Key is just the right length.
pass
return key
def set_info(self, c2s_version_string, s2c_version_string, c2s_kexinit_packet, s2c_kexinit_packet, supported_server_keys):
self.c2s_version_string = c2s_version_string
self.s2c_version_string = s2c_version_string
self.c2s_kexinit_packet = c2s_kexinit_packet
self.s2c_kexinit_packet = s2c_kexinit_packet
self.supported_server_keys = supported_server_keys
def get_key_algorithm(self, key):
name = ssh.util.packet.unpack_payload( (ssh.util.packet.STRING,), key)[0]
for key_alg in self.supported_server_keys:
if key_alg.name == name:
return key_alg
raise ValueError, name
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.key_exchange.diffie_hellman
#
# This module implements the Diffie Hellman Group 1 SHA1 key exchange.
#
__version__ = '$Revision: #1 $'
import hashlib
import ssh.util.debug
import ssh.util.packet
import ssh.util.random
import ssh.key_exchange
import ssh.transport.constants
# 2**1024 - 2**960 - 1 + 2**64 * floor( 2**894 Pi + 129093 )
DH_PRIME = 179769313486231590770839156793787453197860296048756011706444423684197180216158519368947833795864925541502180565485980503646440548199239100050792877003355816639229553136239076508735759914822574862575007425302077447712589550957937778424442426617334727629299387668709205606050270810842907692932019128194467627007L
DH_GENERATOR = 2L
SSH_MSG_KEXDH_INIT = 30
SSH_MSG_KEXDH_REPLY = 31
class Diffie_Hellman_Group1_SHA1(ssh.key_exchange.SSH_Key_Exchange):
name = 'diffie-hellman-group1-sha1'
# What type of host key features this kex algorithm wants.
wants_signature_host_key = 1
wants_encryption_host_key = 0
client_random_value = '' # x
client_exchange_value = 0L # e
server_public_host_key = None # k_s
def get_initial_client_kex_packet(self):
self.transport.debug.write(ssh.util.debug.DEBUG_3, 'get_initial_kex_packet()')
# Send initial key.
# This is x.
self.client_random_value = ssh.util.random.get_random_number(512)
# p is large safe prime (DH_PRIME)
# g is a generator for a subgroup of GF(p) (DH_GENERATOR)
# compute e=g**x mod p
self.client_exchange_value = pow(DH_GENERATOR, self.client_random_value, DH_PRIME)
return ssh.util.packet.pack_payload(KEXDH_INIT_PAYLOAD,
(SSH_MSG_KEXDH_INIT,
self.client_exchange_value)
)
def get_initial_server_kex_packet(self):
raise NotImplementedError
def _get_hash_object(self):
"""_get_hash_object(self) -> hash_object
Return a raw hash object (see get_hash_object).
"""
return hashlib.sha1()
def register_client_callbacks(self):
callbacks = {SSH_MSG_KEXDH_REPLY: self.msg_kexdh_reply}
self.transport.register_callbacks(self.name, callbacks)
def register_server_callbacks(self):
raise NotImplementedError
def msg_kexdh_reply(self, packet):
# string server public host key and certificates (K_S)
# mpint f
# string signature of H
msg, public_host_key, server_exchange_value, signature_of_h = ssh.util.packet.unpack_payload(KEXDH_REPLY_PAYLOAD, packet)
# Create a SSH_Public_Private_Key instance from the packed string.
self.server_public_host_key = ssh.keys.parse_public_key(public_host_key)
# Verify that this is a known host key.
self.transport.verify_public_host_key(self.server_public_host_key)
# Make sure f is a valid number
if server_exchange_value <= 1 or server_exchange_value >= DH_PRIME-1:
self.transport.send_disconnect(ssh.transport.constants.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Key exchange did not succeed: Server exchange value not valid.')
# K = f**x mod p
self.shared_secret = pow(server_exchange_value, self.client_random_value, DH_PRIME)
# Verify hash.
# string V_C, the client's version string (CR and NL excluded)
# string V_S, the server's version string (CR and NL excluded)
# string I_C, the payload of the client's SSH_MSG_KEXINIT
# string I_S, the payload of the server's SSH_MSG_KEXINIT
# string K_S, the host key
# mpint e, exchange value sent by the client
# mpint f, exchange value sent by the server
# mpint K, the shared secret
H = ssh.util.packet.pack_payload(KEXDH_HASH_PAYLOAD,
(self.c2s_version_string,
self.s2c_version_string,
self.c2s_kexinit_packet,
self.s2c_kexinit_packet,
public_host_key,
self.client_exchange_value,
server_exchange_value,
self.shared_secret))
# Double check that the signature from the server matches our signature.
hash = hashlib.sha1(H)
self.exchange_hash = hash.digest()
if self.session_id is None:
# The session id is the first exchange hash.
self.session_id = self.exchange_hash
if not self.server_public_host_key.verify(self.exchange_hash, signature_of_h):
self.transport.send_disconnect(ssh.transport.constants.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Key exchange did not succeed: Signature did not match.')
# Finished...
self.transport.send_newkeys()
KEXDH_REPLY_PAYLOAD = (ssh.util.packet.BYTE,
ssh.util.packet.STRING, # public host key and certificates (K_S)
ssh.util.packet.MPINT, # f
ssh.util.packet.STRING # signature of H
)
KEXDH_INIT_PAYLOAD = (ssh.util.packet.BYTE,
ssh.util.packet.MPINT # e
)
KEXDH_HASH_PAYLOAD = (ssh.util.packet.STRING, # V_C, the client's version string (CR and NL excluded)
ssh.util.packet.STRING, # V_S, the server's version string (CR and NL excluded)
ssh.util.packet.STRING, # I_C, the payload of the client's SSH_MSG_KEXINIT
ssh.util.packet.STRING, # I_S, the payload of the server's SSH_MSG_KEXINIT
ssh.util.packet.STRING, # K_S, the host key
ssh.util.packet.MPINT, # e, exchange value sent by the client
ssh.util.packet.MPINT, # f, exchange value sent by the server
ssh.util.packet.MPINT # K, the shared secret
)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys
#
# This implements the interface to parse and use various different key types.
#
__version__ = '$Revision: #1 $'
import ssh.util.packet
import dss
import rsa
# Map of supported key types.
keytypes = {
'ssh-dss': dss.SSH_DSS,
'ssh-rsa': rsa.SSH_RSA,
}
class Unknown_Key_Type(Exception):
def __init__(self, keytype):
self.keytype = keytype
Exception.__init__(self, keytype)
def __str__(self):
return '<Unknown_Key_Type: %r>' % self.keytype
def parse_public_key(public_key):
"""parse_public_key(public_key) -> SSH_Public_Private_Key instance
This takes a public key and generates an SSH_Public_Private_Key instance.
<public_key>: A packed public key. The format should be a packed string
with the first value being a string to identify the type.
"""
data, offset = ssh.util.packet.unpack_payload_get_offset((ssh.util.packet.STRING,), public_key)
keytype = data[0]
if not keytypes.has_key(keytype):
raise Unknown_Key_Type(keytype)
key_obj = keytypes[keytype]()
key_obj.set_public_key(public_key)
return key_obj
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.ber
#
# Very simple ASN.1 BER decoder used for SSH private keys.
# This implements more than is necessary for SSH, but not the entire
# ASN.1 standard.
#
__version__ = '$Revision: #1 $'
# flags for BER tags
FLAGS_UNIVERSAL = 0x00
FLAGS_STRUCTURED = 0x20
FLAGS_APPLICATION = 0x40
FLAGS_CONTEXT = 0x80
FLAGS_PRIVATE = 0xC0
# universal BER tags
TAGS_INTEGER = 0x02
TAGS_OCTET_STRING = 0x04
TAGS_SEQUENCE = 0x10 | FLAGS_STRUCTURED
class DecodeError (Exception):
pass
class InsufficientData (DecodeError):
pass
class InvalidData (DecodeError):
pass
class UnknownTag (DecodeError):
pass
# SAFETY NOTE: it's important for each decoder to correctly handle length == zero.
def decode_string(s, pos, length):
# caller guarantees sufficient data in <s>
result = s[pos:pos+length]
pos += length
return result, pos
def decode_integer(s, pos, length):
if length == 0:
return 0, pos
else:
n = long(ord(s[pos]))
if n & 0x80:
# negative
n = n - 0x100
length -= 1
while length:
pos += 1
n = (n << 8) | ord(s[pos])
length -= 1
# advance past the last byte
pos += 1
return n, pos
def decode_structured(s, pos, length):
start = pos
end = start + length
result = []
if length:
while pos < end:
item, pos = decode (s, pos, end)
result.append(item)
return result, pos
# can an asn1 string *end* with a length? i.e., can we just do the
# length check once, at the front, and assume at least three bytes?
def decode(s, pos=0, eos=-1):
"""decode(s, pos=0, eos=-1) -> (value, pos)
Decodes a BER-encoded string.
Return value includes the position where decoding stopped.
<pos> start of scanning position.
<eos> end of scanning.
"""
if eos == -1:
eos = len(s)
# 1) get tag
tag = ord(s[pos])
pos += 1
# 2) get length
if pos > eos:
# assure at least one byte [valid for length == 0]
raise InsufficientData, pos
a = ord(s[pos])
if a < 0x80:
# one-byte length
length = a
pos += 1
elif pos + 1 >= eos:
# assure at least two bytes
raise InsufficientData, pos
elif a == 0x81:
# one-byte length (0x80 <= x <= 0xff)
length = ord(s[pos+1])
pos += 2
elif pos + 2 >= eos:
# assure at least three bytes
raise InsufficientData, pos
elif a == 0x82:
# two-byte length (0x80 <= x <= 0xffff)
length = ord(s[pos+1])
length = (length << 8) | ord(s[pos+2])
pos += 3
else:
# longer lengths allowed? >0x82?
raise InvalidData, pos
# 3) get value
# assure at least <length> bytes
if (pos + length) > eos:
raise InsufficientData, pos
elif tag == TAGS_OCTET_STRING:
return decode_string (s, pos, length)
elif tag == TAGS_INTEGER:
return decode_integer (s, pos, length)
elif tag == TAGS_SEQUENCE:
return decode_structured (s, pos, length)
else:
raise UnknownTag, tag
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.dss
#
# Encapsulates the DSS key.
__version__ = '$Revision: #1 $'
import public_private_key
import hashlib
import ssh.util.packet
import ssh.util.random
from Crypto.PublicKey import DSA
from Crypto.Util import number
class SSH_DSS(public_private_key.SSH_Public_Private_Key):
# Features of this key type.
supports_signature = 1
supports_encryption = 0
name = 'ssh-dss'
# p, q, g, y
private_key = (0L, 0L, 0L, 0L, 0L)
# p, q, g, y, x
public_key = (0L, 0L, 0L, 0L)
def set_public_key(self, public_key):
dss, p, q, g, y = ssh.util.packet.unpack_payload(DSS_PUBLIC_KEY_PAYLOAD, public_key)
if dss != 'ssh-dss':
raise ValueError, dss
self.public_key = (p, q, g, y)
set_public_key.__doc__ = public_private_key.SSH_Public_Private_Key.set_public_key.__doc__
def set_private_key(self, private_key):
dss, p, q, g, y, x = ssh.util.packet.unpack_payload(DSS_PRIVATE_KEY_PAYLOAD, private_key)
if dss != 'ssh-dss':
raise ValueError, dss
self.private_key = (p, q, g, y, x)
set_private_key.__doc__ = public_private_key.SSH_Public_Private_Key.set_private_key.__doc__
def get_public_key_blob(self):
p, q, g, y = self.public_key
return ssh.util.packet.pack_payload(DSS_PUBLIC_KEY_PAYLOAD,
('ssh-dss',
p, q, g, y))
get_public_key_blob.__doc__ = public_private_key.SSH_Public_Private_Key.get_public_key_blob.__doc__
def get_private_key_blob(self):
p, q, g, y, x = self.public_key
return ssh.util.packet.pack_payload(DSS_PRIVATE_KEY_PAYLOAD,
('ssh-dss',
p, q, g, y, x))
get_private_key_blob.__doc__ = public_private_key.SSH_Public_Private_Key.get_private_key_blob.__doc__
def sign(self, message):
p, q, g, y, x = self.private_key
dsa_obj = DSA.construct( (y, g, p, q, x) )
message_hash = hashlib.sha1(message).digest()
# Get a random number that is greater than 2 and less than q.
random_number = ssh.util.random.get_random_number_from_range(2, q)
random_data = number.long_to_bytes(random_number)
r, s = dsa_obj.sign(message_hash, random_data)
signature = number.long_to_bytes(r, 20) + number.long_to_bytes(s, 20)
return ssh.util.packet.pack_payload(DSS_SIG_PAYLOAD,
('ssh-dss',
signature))
sign.__doc__ = public_private_key.SSH_Public_Private_Key.sign.__doc__
def verify(self, message, signature):
p, q, g, y = self.public_key
dss, blob = ssh.util.packet.unpack_payload(DSS_SIG_PAYLOAD, signature)
if dss != 'ssh-dss':
raise ValueError, dss
# blob is the concatenation of r and s
# r and s are 160-bit (20-byte) integers in network-byte-order
assert( len(blob) == 40 )
r = number.bytes_to_long(blob[:20])
s = number.bytes_to_long(blob[20:])
dsa_obj = DSA.construct( (y, g, p, q) )
hash_of_message = hashlib.sha1(message).digest()
return dsa_obj.verify(hash_of_message, (r, s))
verify.__doc__ = public_private_key.SSH_Public_Private_Key.verify.__doc__
DSS_PUBLIC_KEY_PAYLOAD = (ssh.util.packet.STRING, # "ssh-dss"
ssh.util.packet.MPINT, # p
ssh.util.packet.MPINT, # q
ssh.util.packet.MPINT, # g
ssh.util.packet.MPINT # y
)
DSS_PRIVATE_KEY_PAYLOAD = (ssh.util.packet.STRING, # "ssh-dss"
ssh.util.packet.MPINT, # p
ssh.util.packet.MPINT, # q
ssh.util.packet.MPINT, # g
ssh.util.packet.MPINT, # y
ssh.util.packet.MPINT, # x
)
DSS_SIG_PAYLOAD = (ssh.util.packet.STRING, # "ssh-dss"
ssh.util.packet.STRING # signature_key_blob
)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.key_storage
#
# This module loads and saves various types of SSH public/private keys.
#
__version__ = '$Revision: #1 $'
class Invalid_Server_Public_Host_Key(Exception):
"""Invalid_Server_Public_Host_Key(host_id, public_host_key)
This exception is raised when we have no knowledge of the server's
public key. Normally what happens is the user is asked if the fingerprint
of the public key is OK.
<host_id>: A Remote_Host_ID instance.
<public_host_key>: A SSH_Public_Private_Key instance.
"""
def __init__(self, host_id, public_host_key):
self.host_id = host_id
self.public_host_key = public_host_key
Exception.__init__(self, host_id, public_host_key)
def __str__(self):
return '<Invalid_Server_Public_host_Key host_id=%s>' % self.host_id
# XXX: include the filename and line number of the conflict.
class Host_Key_Changed_Error(Exception):
"""Host_Key_Changed_Error(host_id, location)
This exception is raised when the server's public host key does not match
our database.
<host_id>: A Remote_Host_ID instance
<location>: A string to direct the user to how to find the offending key
in their local database. May be the empty string if it is
not relevant.
"""
def __init__(self, host_id, location):
self.host_id = host_id
self.location = location
Exception.__init__(self, host_id, location)
def __str__(self):
return '<Host_Key_Changed_Error host_id=%s location=%s>' % (self.host_id, self.location)
class SSH_Key_Storage:
def load_keys(self, username=None, **kwargs):
"""load_keys(self, username=None, **kwargs) -> [private_public_key_obj, ...]
Loads the public and private keys.
<username> defaults to the current user.
Different key storage classes take different arguments.
Returns a list of SSH_Public_Private_Key objects.
Returns an empty list if the key is not available.
"""
raise NotImplementedError
def load_private_keys(self, username=None, **kwargs):
"""load_private_keys(self, username=None, **kwargs) -> [private_key_obj, ...]
Loads the private keys.
<username> defaults to the current user.
Different key storage classes take different arguments.
Returns a list of SSH_Public_Private_Key objects.
Returns an empty list if the key is not available.
"""
raise NotImplementedError
def load_public_keys(self, username=None, **kwargs):
"""load_public_keys(self, username=None, **kwargs) -> [public_key_obj, ...]
Loads the public keys.
<username> defaults to the current user.
Different key storage classes take different arguments.
Returns a list of SSH_Public_Private_Key objects.
Returns an empty list if the key is not available.
"""
raise NotImplementedError
def verify(self, host_id, server_key_types, public_host_key, username=None):
"""verify(self, host_id, server_key_types, public_host_key) -> boolean
This verifies that the given public host key is known.
Returns true if it is OK.
<username>: defaults to the current user.
<server_key_types>: A list of SSH_Public_Private_Key objects that we support.
<public_host_key>: A SSH_Public_Private_Key instance.
<host_id>: Remote_Host_ID instance.
"""
raise NotImplementedError
def update_known_hosts(self, host, public_host_key, username=None):
"""update_known_hosts(self, host, public_host_key, username=None) -> None
Updates the known hosts database for the given user.
<host>: The host string.
<public_host_key>: A SSH_Public_Private_Key instance.
<username>: Defaults to the current user.
"""
raise NotImplementedError
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.openssh_authorized_keys
#
# This module handles the authorized_keys file.
#
__version__ = '$Revision: #1 $'
import os
import re
from rebuild import *
class DuplicateKeyError(Exception):
pass
# Keys are in two formats:
# SSH1:
# [options] bits exponent modulus comment
# SSH2:
# [options] keytype base64_key comment
#
# The options are optional. They never start with a number, nor do they
# contain spaces. It is a comma-separated list of values.
#
# The SSH1 key format is as follows. The bits is a number, typically
# something like 1024. The exponent is also a number, typically something
# like 35. The modulus is a very long string of numbers. The comment can be
# anything, but it is typically username@hostname.
#
# The SSH2 key format is as follows. The keytype is either ssh-dss or ssh-rsa.
# The key is a long string encoded in base-64. The comment can be anything,
# but it is typically username@hostname.
SPACE = '[ \t]'
OPTION_START = '[^ \t0-9]'
ATOM = '[^ \t]'
QUOTED_ATOM = '"[^"]*"'
WORD = OR(ATOM, QUOTED_ATOM)
OPTIONS = NAME('options', OPTION_START + SPLAT(WORD))
BITS = NAME('bits', '\d+')
EXPONENT = NAME('exponent', '\d+')
MODULUS = NAME('modulus', '\d+')
COMMENT = NAME('comment', '.*')
ssh1_key = CONCAT('^',
SPLAT(SPACE),
BITS, PLUS(SPACE),
EXPONENT, PLUS(SPACE),
MODULUS, SPLAT(SPACE),
COMMENT
)
ssh1_key_w_options = CONCAT('^',
SPLAT(SPACE),
OPTIONS, PLUS(SPACE),
BITS, PLUS(SPACE),
EXPONENT, PLUS(SPACE),
MODULUS, SPLAT(SPACE),
COMMENT
)
# The man page for OpenSSH specifies that "ssh-dss" and "ssh-rsa" are the only
# valid types, but the code actually checks for this list of types. Let's
# try to be as flexible as possible.
KEYTYPE = NAME('keytype', OR('ssh-dss', 'ssh-rsa', 'rsa1', 'rsa', 'dsa'))
# Not a very exact base64 regex, but should be good enough.
# OpenSSH seems to ignore spaces anywhere. Also, this doesn't check for
# a "partial" or truncated base64 string.
BASE64_KEY = NAME('base64_key', PLUS('[a-zA-Z0-9+/=]'))
ssh2_key = CONCAT('^',
SPLAT(SPACE),
KEYTYPE, PLUS(SPACE),
BASE64_KEY, SPLAT(SPACE),
COMMENT
)
ssh2_key_w_options = CONCAT('^',
SPLAT(SPACE),
OPTIONS, PLUS(SPACE),
KEYTYPE, PLUS(SPACE),
BASE64_KEY, SPLAT(SPACE),
COMMENT
)
ssh1_key_re = re.compile(ssh1_key)
ssh2_key_re = re.compile(ssh2_key)
ssh1_key_w_options_re = re.compile(ssh1_key_w_options)
ssh2_key_w_options_re = re.compile(ssh2_key_w_options)
class OpenSSH_Authorized_Keys:
"""OpenSSH_Authorized_Keys(filename)
This is a class that will represent an SSH authorized_keys file.
"""
def __init__(self, filename):
self.filename = filename
# This is a list of dictionary objects.
self.keys = []
self.read()
def read(self):
"""read() -> None
Reads the contents of the keyfile into memory.
If the file does not exist, then it does nothing.
"""
if os.path.exists(self.filename):
lines = open(self.filename).readlines()
else:
lines = []
for line in lines:
line = line.strip()
# ignore comment lines
if line and line[0] != '#':
try:
self.add_key(line)
except (DuplicateKeyError, ValueError):
# Ignore this entry.
# Maybe we should print an error or something?
pass
def add_key(self, key):
"""add_key(key) -> None
Adds the given key to the object.
<key> is a string.
Raises DuplicateKeyError if the key already exists.
Raises ValueError if the key does not appear to be a valid format.
"""
key = key.strip()
m = ssh1_key_re.match(key)
if not m:
m = ssh2_key_re.match(key)
if not m:
m = ssh1_key_w_options_re.match(key)
if not m:
m = ssh2_key_w_options_re.match(key)
if not m:
raise ValueError, key
values = m.groupdict()
if ((values.has_key('keytype') and not values['keytype']) or
(values.has_key('base64_key') and not values['base64_key']) or
(values.has_key('bits') and not values['bits']) or
(values.has_key('exponent') and not values['exponent']) or
(values.has_key('modulus') and not values['modulus'])
):
raise ValueError, key
self._value_strip(values)
if not values.has_key('options') or not values['options']:
# If it doesn't exist, or it exists as None, set it to the empty string.
values['options'] = ''
if not values['comment']:
values['comment'] = ''
self._duplicate_check(values)
self.keys.append(values)
def _value_strip(self, d):
"""_value_strip(d) -> None
Takes d, which is a dict, and calls strip() on all its values.
"""
for key, value in d.items():
if value:
d[key] = value.strip()
def _duplicate_check(self, key):
"""_duplicate_check(key) -> None
Checks if key (which is dict-format) is a duplicate.
Raises DuplicateKeyError if it is.
"""
if key.has_key('bits'):
# SSH1
for x in self.keys:
if (x.has_key('bits') and
x['bits'] == key['bits'] and
x['exponent'] == key['exponent'] and
x['modulus'] == key['modulus']):
raise DuplicateKeyError
else:
# SSH2
for x in self.keys:
if (x.has_key('keytype') and
x['keytype'] == key['keytype'] and
x['base64_key'] == key['base64_key']):
raise DuplicateKeyError
def write(self):
"""write() -> None
Writes the keyfile to disk, safely overwriting the keyfile that
already exists.
"""
# Avoid concurrent races here?
tmp_filename = self.filename + '.tmp'
fd = os.open(tmp_filename, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0644)
write = lambda x,y=fd: os.write(y, x)
map(write, map(self.keydict_to_string, self.keys))
os.close(fd)
os.rename(tmp_filename, self.filename)
def keydict_to_string(self, key, short_output=0):
"""keydict_to_string(key, short_output=0) -> string
Converts an SSH dict-format key into a string.
<short_output> - Set to true if you want to exclude options and comment.
"""
if short_output:
options = ''
comment = ''
else:
options = key['options']
comment = key['comment']
if key.has_key('bits'):
# SSH1
bits = key['bits']
exponent = key['exponent']
modulus = key['modulus']
result = ' '.join([options, bits, exponent, modulus, comment])
else:
# SSH2
keytype = key['keytype']
base64_key = key['base64_key']
result = ' '.join([options, keytype, base64_key, comment])
return result.strip() + '\n'
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.openssh_key_formats
#
# This module contains expressions for parsing and matching keys.
#
__version__ = '$Revision: #1 $'
import re
from rebuild import *
SPACE = '[ \t]'
NUMBER = '\d'
LIST_OF_HOSTS = NAME('list_of_hosts', PLUS('[^ \t]'))
COMMENT = OPTIONAL(
PLUS(SPACE),
NAME('comment', PLUS('.'))
)
ssh1_key = re.compile(
CONCAT('^',
LIST_OF_HOSTS,
PLUS(SPACE),
NAME('number_of_bits', PLUS(NUMBER)),
PLUS(SPACE),
NAME('exponent', PLUS(NUMBER)),
PLUS(SPACE),
NAME('modulus', PLUS(NUMBER)),
COMMENT
)
)
# The man page for OpenSSH specifies that "ssh-dss" and "ssh-rsa" are the only
# valid types, but the code actually checks for this list of types. Let's
# try to be as flexible as possible.
KEYTYPE = NAME('keytype', OR('ssh-dss', 'ssh-rsa', 'rsa1', 'rsa', 'dsa'))
# Not a very exact base64 regex, but should be good enough.
# OpenSSH seems to ignore spaces anywhere. Also, this doesn't check for
# a "partial" or truncated base64 string.
BASE64_KEY = NAME('base64_key', PLUS('[a-zA-Z0-9+/=]'))
ssh2_key = re.compile(
CONCAT('^',
KEYTYPE,
PLUS(SPACE),
BASE64_KEY,
COMMENT
)
)
ssh2_known_hosts_entry = re.compile(
CONCAT('^',
LIST_OF_HOSTS,
PLUS(SPACE),
KEYTYPE,
PLUS(SPACE),
BASE64_KEY,
COMMENT
)
)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.openssh_key_storage
#
# This module is capable of loading key files that are generated by OpenSSH.
#
# XXX: Make key parse error exception. Replace asserts.
__version__ = '$Revision: #1 $'
import key_storage
import binascii
import hashlib
import os
import re
import rebuild
import ber
import dss
import rsa
import ssh.util
import ssh.util.password
import ssh.keys.openssh_key_formats
from Crypto.Cipher import DES
import openssh_known_hosts
import remote_host
class OpenSSH_Key_Storage(key_storage.SSH_Key_Storage):
header = re.compile(
rebuild.CONCAT(
rebuild.NAME('name', '[^:]*'),
':',
rebuild.SPLAT('[ \t]'),
rebuild.NAME('value', '.*')
)
)
key_types = ('dsa', 'rsa')
def get_private_key_filenames(self, username, private_key_filename):
"""get_private_key_filenames(self, username, private_key_filename) -> [filename, ...]
Gets the filenames of the private keys.
<username> - Look into the home directory of this user for the key.
If None, uses the current user.
<private_key_filename> - If this is set, then this is the value to
return. Otherwise the filename is computed from the
username's home directory. This is provided as a
convenience to handle the situation where the filename
is forced to a specific value.
"""
if private_key_filename is None:
if username is None:
username = os.getlogin()
home_dir = os.path.expanduser('~' + username)
result = []
for key_type in self.key_types:
private_key_filename = os.path.join(home_dir, '.ssh', 'id_%s' % key_type)
result.append(private_key_filename)
return result
else:
return [private_key_filename]
get_private_key_filenames = classmethod(get_private_key_filenames)
def get_public_key_filenames(self, username, public_key_filename):
"""get_public_key_filenames(self, username, public_key_filename) -> [filename, ...]
Gets the filenames of the public keys.
<username> - Look into the home directory of this user for the key.
If None, uses the current user.
<private_key_filename> - If this is set, then this is the value to
return. Otherwise the filename is computed from the
username's home directory. This is provided as a
convenience to handle the situation where the filename
is forced to a specific value.
"""
if public_key_filename is None:
result = self.get_private_key_filenames(username, None)
result = map(lambda x: x+'.pub', result)
return result
else:
return [public_key_filename]
get_public_key_filenames = classmethod(get_public_key_filenames)
def load_keys(self, username=None, private_key_filename=None, public_key_filename=None):
"""load_keys(self, username=None, private_key_filename=None, public_key_filename=None) -> [public_private_key_obj, ...]
Loads both the private and public keys. Returns a list of
SSH_Public_Private_Key object. Returns an empty list if both the
public and private keys are not available.
<private_key_filename> - defaults to $HOME/.ssh/id_dsa or id_rsa.
<public_key_filename> - If set to None, then it will assume the
filename is the same as
<private_key_filename> with a .pub extension.
<username> - Look into the home directory of this user for the key.
If None, uses the current user.
"""
private_key_filenames = self.get_private_key_filenames(username, private_key_filename)
result = []
for filename in private_key_filenames:
private_keys = self.load_private_keys(private_key_filename=filename)
if private_keys:
assert (len(private_keys) == 1)
if public_key_filename is None:
pub_filename = filename + '.pub'
else:
pub_filename = public_key_filename
public_keys = self.load_public_keys(public_key_filename=pub_filename)
if public_keys:
assert (len(public_keys) == 1)
# Join the two keys into one.
key = private_keys[0]
key.public_key = public_keys[0].public_key
result.append(key)
return result
def load_private_keys(self, username=None, private_key_filename=None):
"""load_private_keys(self, username=None, private_key_filename=None) -> [key_obj, ...]
Loads the private keys with the given filename.
Defaults to $HOME/.ssh/id_dsa or id_rsa
Returns a list SSH_Public_Private_Key object.
Returns an empty list if the key is not available.
"""
private_key_filenames = self.get_private_key_filenames(username, private_key_filename)
result = []
for filename in private_key_filenames:
try:
data = open(filename).read()
except IOError:
pass
else:
result.append(self.parse_private_key(data))
return result
load_private_keys = classmethod(load_private_keys)
def load_public_keys(self, username=None, public_key_filename=None):
"""load_public_keys(self, username=None, public_key_filename=None) -> [key_obj, ...]
Loads the public keys with the given filename.
Defaults to $HOME/.ssh/id_dsa.pub
Returns a list of SSH_Public_Private_Key object.
Returns an empty list if the key is not available.
"""
public_key_filenames = self.get_public_key_filenames(username, public_key_filename)
result = []
for filename in public_key_filenames:
try:
data = open(filename).read()
except IOError:
pass
else:
result.append(self.parse_public_key(data))
return result
load_public_keys = classmethod(load_public_keys)
def parse_private_key(self, private_key):
"""parse_private_key(self, private_key) -> key_obj
Parses the given string into an SSH_Public_Private_Key object.
"""
# Format (PEM which is RFC 1421):
# -----BEGIN DSA PRIVATE KEY-----
# RFC 822 headers.
# keydata_base64
# -----END DSA PRIVATE KEY-----
# keydata is BER-encoded
data = private_key.split('\n')
self._strip_empty_surrounding_lines(data)
for key_type in self.key_types:
if (data[0] == '-----BEGIN %s PRIVATE KEY-----' % (key_type.upper(),) and
data[-1] == '-----END %s PRIVATE KEY-----' % (key_type.upper(),)):
break
else:
raise ValueError, 'Corrupt key header/footer format: %s %s' % (data[0],data[-1])
key_data = []
# XXX: Does not support multiple headers with the same name.
headers = {}
current_line = 1
if ':' in data[current_line]:
# starts with RFC 822 headers
continuation = 0 # Flag to follow continuation line
current_value = []
name = '' # pychecker squelch
while 1:
line = data[current_line]
if line.startswith(' ') or line.startswith('\t') or continuation:
if line.endswith('\\'):
# Strip trailing slash.
line = line[:-1]
continuation = 1
else:
continuation = 0
current_value.append(line.lstrip())
current_line += 1
continue
else:
if current_value:
headers[name] = ''.join(current_value)
current_value = []
if not line:
# end of headers
break
match = self.header.match(line)
assert (match != None), 'Invalid header value in private key: %r' % line
d = match.groupdict()
name = d['name']
value = d['value']
if line.endswith('\\'):
# Continuation (see ietf-secsh-publickeyfile)
continuation = 1
current_value.append(value)
current_line += 1
# Parse the key
while 1:
if data[current_line].startswith('-----'):
break
if data[current_line]:
key_data.append(data[current_line])
current_line += 1
key_data = ''.join(key_data)
key_data = binascii.a2b_base64(key_data)
if headers.has_key('Proc-Type'):
proc_type = headers['Proc-Type'].split(',')
proc_type = map(lambda x: x.strip(), proc_type)
if len(proc_type)==2 and proc_type[0]=='4' and proc_type[1]=='ENCRYPTED':
# Key is encrypted.
assert headers.has_key('DEK-Info'), 'Private key missing DEK-Info field.'
dek_info = headers['DEK-Info'].split(',')
dek_info = map(lambda x: x.strip(), dek_info)
assert (len(dek_info) == 2), 'Expected two values in DEK-Info field: %r' % dek_info
# XXX: Do we need to support more encryption types?
assert (dek_info[0] == 'DES-EDE3-CBC'), 'Can only handle DES-EDE3-CBC encryption: %r' % dek_info[0]
iv = binascii.a2b_hex(dek_info[1])
passphrase = self.ask_for_passphrase()
# Convert passphrase to a key.
a = hashlib.md5(passphrase + iv).digest()
b = hashlib.md5(a + passphrase + iv).digest()
passkey = (a+b)[:24] # Only need first 24 characters.
key_data = self.des_ede3_cbc_decrypt(key_data, iv, passkey)
key_data = ber.decode(key_data)[0]
# key_data[0] is always 0???
if not keytype_map.has_key(key_type):
return None
key_obj = keytype_map[key_type]()
# Just so happens both dsa and rsa keys have 5 numbers.
key_obj.private_key = tuple(key_data[1:6])
return key_obj
parse_private_key = classmethod(parse_private_key)
def ask_for_passphrase():
return ssh.util.password.get_password('Enter passphrase> ')
ask_for_passphrase = staticmethod(ask_for_passphrase)
def des_ede3_cbc_decrypt(data, iv, key):
assert (len(data) % 8 == 0), 'Data block must be a multiple of 8: %i' % len(data)
key1 = DES.new(key[0:8], DES.MODE_ECB)
key2 = DES.new(key[8:16], DES.MODE_ECB)
key3 = DES.new(key[16:24], DES.MODE_ECB)
# Outer-CBC Mode
# 8-byte blocks
result = []
prev = iv
for i in xrange(0, len(data), 8):
block = data[i:i+8]
value = key1.decrypt(
key2.encrypt(
key3.decrypt(block)))
result.append(ssh.util.str_xor(prev, value))
prev = block
return ''.join(result)
des_ede3_cbc_decrypt = staticmethod(des_ede3_cbc_decrypt)
def parse_public_key(public_key):
"""parse_public_key(public_key) -> key_obj
Parses the given string into an SSH_Public_Private_Key object.
Returns None if parsing fails.
<public_key>: The public key as a base64 string.
"""
# Format:
# keytype SPACE+ base64_string [ SPACE+ comment ]
key_match = ssh.keys.openssh_key_formats.ssh2_key.match(public_key)
if not key_match:
return None
keytype = key_match.group('keytype')
if not keytype_map.has_key(keytype):
return None
try:
key = binascii.a2b_base64(key_match.group('base64_key'))
except binascii.Error:
return None
key_obj = keytype_map[keytype]()
key_obj.set_public_key(key)
return key_obj
parse_public_key = staticmethod(parse_public_key)
def _strip_empty_surrounding_lines(data):
while 1:
if not data[0]:
del data[0]
else:
break
while 1:
if not data[-1]:
del data[-1]
else:
break
_strip_empty_surrounding_lines = staticmethod(_strip_empty_surrounding_lines)
def get_authorized_keys_filename(username, authorized_keys_filename=None):
if authorized_keys_filename is None:
if username is None:
username = os.getlogin()
home_dir = os.path.expanduser('~' + username)
authorized_keys_filename = os.path.join(home_dir, '.ssh', 'authorized_keys')
return authorized_keys_filename
get_authorized_keys_filename = staticmethod(get_authorized_keys_filename)
def verify(self, host_id, server_key_types, public_host_key, username=None):
for key in server_key_types:
if public_host_key.name == key.name:
# This is a supported key type.
if self._verify_contains(host_id, public_host_key, username):
return 1
return 0
verify.__doc__ = key_storage.SSH_Key_Storage.verify.__doc__
verify = classmethod(verify)
def _verify_contains(host_id, key, username):
"""_verify_contains(host_id, key, username) -> boolean
Checks whether <key> is in the known_hosts file.
"""
# Currently only supported IPv4
if not isinstance(host_id, remote_host.IPv4_Remote_Host_ID):
return 0
hostfile = openssh_known_hosts.OpenSSH_Known_Hosts()
return hostfile.check_for_host(host_id, key, username)
_verify_contains = staticmethod(_verify_contains)
def update_known_hosts(host, public_host_key, username=None):
hostfile = openssh_known_hosts.OpenSSH_Known_Hosts()
hostfile.update_known_hosts(host, public_host_key, username)
update_known_hosts.__doc__ = key_storage.SSH_Key_Storage.update_known_hosts.__doc__
update_known_hosts = staticmethod(update_known_hosts)
keytype_map = {'ssh-dss': dss.SSH_DSS,
'dss': dss.SSH_DSS,
'dsa': dss.SSH_DSS,
'ssh-rsa': rsa.SSH_RSA,
'rsa': rsa.SSH_RSA,
# 'rsa1': None
}
import unittest
class ssh_key_storage_test_case(unittest.TestCase):
pass
class load_dsa_test_case(ssh_key_storage_test_case):
def runTest(self):
public_key = 'ssh-dss AAAAB3NzaC1kc3MAAACBAM46u7kMaoOESTiF3fwqvKry2YSYwlgcl2fRtw5IBgLyeS5SLy/M18ZeGLBokFSAFN110B4X6mUK05VMn3KGo0xKnu35+s4g20vOn9ubjXzUkt4EORJZ+MPPaQOllW22m5fjutND3SzahUOx9Z/PaTSbRLGovpTA7NjlliUVt32rAAAAFQCgqkv3v9z16r0z36InixKZTeWcIQAAAIB7qsZKumVthTLCzj/nAgOvdehLm8PbpWAYe8g1QyAGhbyB0MTwak0TvtBrxCq1nbCkYuFdPVtAWw7Q6fk4nf+3vNiKIl55lVMmUpJ2KkGBJDuEuMUWPRiiJZwW+KxKUyB7pKY5gwJt4DLGlfVjQW4q+b0qm83k/XUoW3VW/L4TIAAAAIAQbMUcClGzedoL7bIf4vh7DiQedlMaTM66EL8awJAQBNfAc9au84J0yMz84/6Dub2h+XwP6Ip5E+QjD32grBgj2MV3orjeXa3GKEbmLV9+3asZKma+gzfQurz0rfR767vp5p4ZScODAp/u64FrMQeiMLD0TePAOhDX7Y6ON5AOlw== admin@test04.god\n'
public_key_value = (144819228510396375480510966045726324197234443151241728654670685625305230385467763734653299992854300412367868856607501321634131298084648429649714452472261648519166487595581105734370788168033696455943547609540069712392591019911289209306656760054646817215504894551439102079913490941604156000063251698742214491563L,
917236267741783881593757959752012731596818193441L,
86841982599782731711680786695115998714268381589063264016290973272276727186587704621026934440034006184167355751218909451695519359588918346763818149790632054843515968619897385312275286742098283669267848018249714288204339152924266843441212538369167733167339150986744361013446636969482251895247384926570829189920L,
11533944838210201987952882615702205024058326377484185868096298195008186074503068022497845512065175251915059614398323642650025427428897101740618300183253025704503580499048317459491367465314627384976328186431247669440664650252937901083193085980371291330875135658464646502031914168659603641006946305696513134231L)
private_key = """-----BEGIN DSA PRIVATE KEY-----
MIIBuwIBAAKBgQDOOru5DGqDhEk4hd38Kryq8tmEmMJYHJdn0bcOSAYC8nkuUi8v
zNfGXhiwaJBUgBTdddAeF+plCtOVTJ9yhqNMSp7t+frOINtLzp/bm4181JLeBDkS
WfjDz2kDpZVttpuX47rTQ90s2oVDsfWfz2k0m0SxqL6UwOzY5ZYlFbd9qwIVAKCq
S/e/3PXqvTPfoieLEplN5ZwhAoGAe6rGSrplbYUyws4/5wIDr3XoS5vD26VgGHvI
NUMgBoW8gdDE8GpNE77Qa8QqtZ2wpGLhXT1bQFsO0On5OJ3/t7zYiiJeeZVTJlKS
dipBgSQ7hLjFFj0YoiWcFvisSlMge6SmOYMCbeAyxpX1Y0FuKvm9KpvN5P11KFt1
Vvy+EyACgYAQbMUcClGzedoL7bIf4vh7DiQedlMaTM66EL8awJAQBNfAc9au84J0
yMz84/6Dub2h+XwP6Ip5E+QjD32grBgj2MV3orjeXa3GKEbmLV9+3asZKma+gzfQ
urz0rfR767vp5p4ZScODAp/u64FrMQeiMLD0TePAOhDX7Y6ON5AOlwIVAIJE+2W3
jbdJzPIVCZV/ns8QD/HE
-----END DSA PRIVATE KEY-----"""
private_key_value = (144819228510396375480510966045726324197234443151241728654670685625305230385467763734653299992854300412367868856607501321634131298084648429649714452472261648519166487595581105734370788168033696455943547609540069712392591019911289209306656760054646817215504894551439102079913490941604156000063251698742214491563L,
917236267741783881593757959752012731596818193441L,
86841982599782731711680786695115998714268381589063264016290973272276727186587704621026934440034006184167355751218909451695519359588918346763818149790632054843515968619897385312275286742098283669267848018249714288204339152924266843441212538369167733167339150986744361013446636969482251895247384926570829189920L,
11533944838210201987952882615702205024058326377484185868096298195008186074503068022497845512065175251915059614398323642650025427428897101740618300183253025704503580499048317459491367465314627384976328186431247669440664650252937901083193085980371291330875135658464646502031914168659603641006946305696513134231L,
743707150676871705974360193988282239149564490180L)
encrypted_private_key = """-----BEGIN DSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,4D88293673F5EB5A
z9/jHRxmgWErlTqA4+BsGWLnFFYJAUSDKLv2mbB7vsTkdHggJGUFm370hXR494R2
kV8zTcdl8xNaD5O9wv2TxAa+1XAK9PMvoZ22EicJkJfdVZB1WxOEo6gYafcbUwn5
jCw2WtOdmFL1LfBTKJUQsN3+s/Z/8xIFjHyZ1AqBEsbtvyT5x6gTCb5gqd9aLmAy
U1MSAS69G83XS0vwGjUBu6hIY+NqH97MYaYuUxYRHZ4kwx6a+AZA05VAWuqCNG/i
REnwjW62umnal6bAb/P0ShV/5Q1N+jOgfbAVeSbwOBNi6l0a3R2vsACowBsQGIOs
AI0LkOljn0SQ9EbiVFt0X3EmDDqXJ4pyUQiQWWVqk/NdlcOXlVHxW1LAH178prSh
9lynvklH9ddxf1ogZzoklnwbHFOQL82VP3OgHzLe4zHEZb7/7n04Hsn65tE1IDPN
BZCMWNmWn5b4XlpvM9qmrqw7OSHXJmo3pUuobgMDJY8ivajqqgLnKobNUyRqIIXe
K+HsnOddon8EQ7paJXiQtIoduGkNprkteopuVCTPVnJ7iPH7nlZlklRzMd0Nf6HX
uUG2MBh5S6IgJq3XFEqkLfnz1kLZTEqa
-----END DSA PRIVATE KEY-----"""
encrypted_private_key_value = (164075852029082894163234846758911180897102424744594692831708895466836370115659341783053385481002813494629146363751701674649183075680471787603685864085093058760568072639047095709834084191969571447062510635065846311615327171352818023633056996941311207910795837452601272338087606069040250703385973352638645677247L,
1382042759715880151069055791721895992148320772021L,
153926732596083235894968118340186482799172595631686494839668449588513699006316353000942531708584009247814149841973530532258546565044160391136120217307693710508485285457287378935130848066293114662648349996271228116599598513247673155335538020265820918414007609749609438690078201904948848717674434559570846935983L,
72384903337992313747768521223976137917296105324177373979721513786321354786021736609201334497124282719580897296769614546413169191983285786187571827959205578548773194544237603360013637593100703429306046520179789796054440223880857876564434231726404752178503857092191315752338544897832163988229715776540342317876L,
863501795884281323360678431361598105234793720895L)
a = OpenSSH_Key_Storage()
dss_obj = a.parse_public_key(public_key)
self.assertEqual(dss_obj.public_key, public_key_value)
dss_obj = a.parse_private_key(private_key)
self.assertEqual(dss_obj.private_key, private_key_value)
# Try an encrypted private key.
class fixed_passphrase_OpenSSH_Key_Storage(OpenSSH_Key_Storage):
def ask_for_passphrase():
return 'foobar'
ask_for_passphrase = staticmethod(ask_for_passphrase)
a = fixed_passphrase_OpenSSH_Key_Storage()
dss_obj = a.parse_private_key(encrypted_private_key)
self.assertEqual(dss_obj.private_key, encrypted_private_key_value)
class load_rsa_test_case(ssh_key_storage_test_case):
def runTest(self):
public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAqmjoccK1YhAhSC4TzycQ/61EtlbbeoeJDdaK03523oNbLdxw5snrsu1cpiR6xlPo5hBsaVBOvBeoQt+SrGwTy12P76i0LCyj6G+ylyHelWG5AsH4PyOETZonEaQiGozCHFXjZ4s/fQ0JjJ55zmhGgqpQNnz2SmqpjZjCipJX70c= admin@test04.god\n'
public_key_value = (35L,
119665828850037267028328680471142741797655500899213447140209093165296153783302091227178836947120698323025616662599207205000714031772597163041046686901539652734222225828489682382176199509894978250917122539502090949162883932641842735933478535393127460177054018760505497941735313342238826050943434409531493838663L)
private_key = """-----BEGIN RSA PRIVATE KEY-----
MIICWgIBAAKBgQCqaOhxwrViECFILhPPJxD/rUS2Vtt6h4kN1orTfnbeg1st3HDm
yeuy7VymJHrGU+jmEGxpUE68F6hC35KsbBPLXY/vqLQsLKPob7KXId6VYbkCwfg/
I4RNmicRpCIajMIcVeNniz99DQmMnnnOaEaCqlA2fPZKaqmNmMKKklfvRwIBIwKB
gD9LiYlW8unou+ecFfx8OYOJf+v0YCYyV3orHZ8DFjVj/UuMZHL6ir7NMQqClACF
kQT+yS5uSSFKnZUueE6rzNXmW+TSXbkYx+Ews60gC1gkbKpV45oKZhg1yfRErwOy
JF8VPQzstIZdf0iWU3uQ+6T64CrPLz0c40b5I47ux5mbAkEA3r2aeIQsftWedjgx
+mXwtCJb1+3oOcbz3tPoZGTqvLWReuQ5ZmZIj06Lg3cy3sNH5+pjDCmiQlH8Eo7E
fFM8jwJBAMPa7SEzquE9/2KqV/iVLYaZedtqfRyDGIz4XIbSTT80dyuplTFpVJWz
Fks1ie97Q4FswqV03D0HXEnnu2vD7ckCQHjqla8jLhj3nyo7w1wLdAoEBfjgPDyf
M+3+AdAZhr42rg+DNRpUyE3LjZCCiVRbYYyGjYpCfKeo2UvnGjTcunkCQBDJn0v9
HUaBqC0HSV5zL8m1Yjdg5icD7ClXHd+roDieGNfbVe5K211J3VbnVPdFFGot5Mxa
eUcPQmy8F2ECKlMCQQDX7sV4IqQrPXGC3TpHXNMJfRy+RuGNFH3mf5j3iAhgnSc5
Sk5Qdbor2yjwE/GHz2ycGtkjRulOUTv4TYX5+O6u
-----END RSA PRIVATE KEY-----"""
private_key_value = (119665828850037267028328680471142741797655500899213447140209093165296153783302091227178836947120698323025616662599207205000714031772597163041046686901539652734222225828489682382176199509894978250917122539502090949162883932641842735933478535393127460177054018760505497941735313342238826050943434409531493838663L,
35L,
44447307858585270610522081317853018381986328905422137509220520318538571405226491027237853723216259377123800474679705533285979497515536089129531626563429005729644097293794274690875731684502848992888319583322382841862406829655518405505486920254346175585047037778915301016757980191172721050366334657821864860059L,
11665873813839313890672267141763021801562202172556393999332152934535676204166852333822550370230102963227543021845798110514178180132425544663462167708908687L,
10257768150044345052157551318596288583246323320335701676273534238701739242861950987619850286757345117997197327072483122544803209060028258639176304917999049L)
encrypted_private_key = """-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,09F6C5E0A7DE5062
12K8ifZGdXBydKq73mrFhrZ3MQvesgCHnSF5DAFt6xka89S7LC8fGxyxKtUNxewF
SZ+9m6A+/ZvD5vsI8xSicdEToFKDIzjBdPg6/2c2PZ5qQn46fQUjoRh3HVxMwe3j
y2c8s9z62aprf6P+QrllcI2h1dSoGpiS2v/CaeZSAo+cDnULrX7ICraVLx7TMdsz
vXuaf4Qa+CcunYOl9fSMAATUZD3LiRAICLZxcsT7MaqVLerWquZ54kOnOLAXMMDb
bth0+DlErt7Er2zjCWz+GSZY5QG663FTBVVVhgQGrj3D9T9VgvMVOneqJHGgkHB8
OYP5Lzw3ukNxlxP8L5Is22S7dGqHNYAoqecS8l6kPkrChRosVHcyl0WViKKuUacm
Oeeh7bYiQNtWy7yXWfwbA/qV46rQqb9jvoK0X0poL1QxLnayZtE2Py5AwGT17MLE
Fgcf5aXRk8BlEhS7Cxx7bTFfgC4reV/SL6D+bWYU91YuWx+Ivr1W/WF7JGBhY+PC
PuYX4L0U1btWcj5Y35ZZQX3iLM5Qo+39gL8YJ8Ee+F51MEu89yuSJrxpbanec55r
iawSKr0WOyY44GA3sfRGKbr6EN56QoR938S0nVAwYCPqwJz00+7ElpLNgH4Utjwj
68pP4jQTVGgI7K4gxN2jDvlol/dTprmjXmyHykW7s7s5Ew4wrN+qMFpgeyIz9/qc
5L0OtjBAbjyLFdE0Xngg0Lmn2bIlvL8jrMPGwaxqu2T0ulrLN8Z2G+1iAafj8Kqh
VaPIN0x5mSV39WpxGz4SmIOVZdIlUL9dOJEv4K4qOHQ=
-----END RSA PRIVATE KEY-----"""
encrypted_private_key_value = (132188032201840059483513934225114077156533953079152741406965569217951447670952039954408688762434667064008713729945007603706264832329500562212083089065564763658016112303811971922789074398850943636984349987490511238526103841681580491336332634066855256218075929727422563972932130562723997143531042508709389080193L,
35L,
86866421161209181946309156776503536417150883452014658638863088343225237040911340541468566901028495499205726165392433568149831175530814655167940315671656829536913792735395949882296110745152030053580749430717046323948708357601049256155974132066189771161275225056736507873625291084649019736200813189569105066383L,
12385928121051751494808912011530512750393016832123879182480209182965942204322814452624601155140075558662348021403304814310741397421628588564328911043663179L,
10672436567524284031640817763908324234466779420903824519134916712505476909909154093286756280899157968011858280387816948898605556328090289066804367098576867L)
a = OpenSSH_Key_Storage()
rsa_obj = a.parse_public_key(public_key)
self.assertEqual(rsa_obj.public_key, public_key_value)
rsa_obj = a.parse_private_key(private_key)
self.assertEqual(rsa_obj.private_key, private_key_value)
# Try an encrypted private key.
class fixed_passphrase_OpenSSH_Key_Storage(OpenSSH_Key_Storage):
def ask_for_passphrase():
return 'foobar'
ask_for_passphrase = staticmethod(ask_for_passphrase)
a = fixed_passphrase_OpenSSH_Key_Storage()
rsa_obj = a.parse_private_key(encrypted_private_key)
self.assertEqual(rsa_obj.private_key, encrypted_private_key_value)
def suite():
suite = unittest.TestSuite()
suite.addTest(load_dsa_test_case())
suite.addTest(load_rsa_test_case())
return suite
if __name__ == '__main__':
unittest.main(defaultTest='suite')
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.openssh_known_hosts
#
# This module handles the known_hosts file.
#
__version__ = '$Revision: #1 $'
# The known_hosts file has the following format:
# Each line contains a key with the following fields (space separated):
# SSH2:
# list_of_hosts, keytype, key, comment
# SSH1:
# list_of_hosts, number_of_bits, exponent, modulus, comment
#
# hosts is a comma-separated list of hosts.
# '*' and '?' are allowed wildcards.
# A hostname starting with '!' means negation. If a hostname matches a
# negated pattern, it is not accepted (by that line) even if it matched
# another pattern on the line.
#
# Lines starting with '#' are comments.
# The known_hosts file is found in $HOME/.ssh/known_hosts or
# in $SSHDIR/ssh_known_hosts.
#
# As a historical note, OpenSSH used to have files called known_hosts2
# (or ssh_known_hosts2 for the system-wide version). This implementation
# does not try to load these copies, since that technique is quite antiquated.
import base64
import binascii
import errno
import os
import re
import ssh.keys.openssh_key_storage
import ssh.keys.dss
import ssh.keys.rsa
import ssh.keys.openssh_key_formats
from ssh.keys.key_storage import Host_Key_Changed_Error
from ssh.keys.remote_host import IPv4_Remote_Host_ID
class OpenSSH_Known_Hosts:
def __init__(self):
pass
def get_known_hosts_filenames(self, username):
# XXX: Support system-wide copy.
return [self.get_users_known_hosts_filename(username)]
def get_users_known_hosts_filename(self, username):
if username is None:
username = os.getlogin()
home_dir = os.path.expanduser('~' + username)
user_known_hosts_filename = os.path.join(home_dir, '.ssh', 'known_hosts')
return user_known_hosts_filename
def check_for_host(self, host_id, key, username=None):
"""check_for_host(self, host_id, key, username=None) -> boolean
Checks if the given key is in the known_hosts file.
Returns true if it is, otherwise returns false.
If the host was found, but the key did not match, it raises a
Host_Key_Changed_Error exception.
<username> - May be None to use the current user.
<host_id> - A IPv4_Remote_Host_ID instance.
<key> - A SSH_Public_Private_Key instance.
"""
if not isinstance(host_id, IPv4_Remote_Host_ID):
raise TypeError, host_id
if host_id.hostname is not None:
hosts = [host_id.ip, host_id.hostname]
else:
hosts = [host_id.ip]
# Changed is a variable to detect a Host_Key_Changed_Error.
# We store away that the error has occurred so that we can allow
# other files to potentially have a correct copy.
changed = None
for filename in self.get_known_hosts_filenames(username):
for host in hosts:
try:
if self._check_for_host(filename, host_id, host, key):
return 1
except Host_Key_Changed_Error, e:
changed = e
if changed is None:
return 0
else:
raise changed
def _check_for_host(self, filename, host_id, host, key):
try:
f = open(filename)
except IOError:
return 0
changed = None
line_number = 0
for line in f.readlines():
line_number += 1
line = line.strip()
if len(line)==0 or line[0]=='#':
continue
m = ssh.keys.openssh_key_formats.ssh2_known_hosts_entry.match(line)
if m:
if key.name == m.group('keytype'):
if self._match_host(host, m.group('list_of_hosts')):
if self._match_key(key, m.group('base64_key')):
return 1
else:
# Found a conflicting key.
changed = Host_Key_Changed_Error(host_id, '%s:%i' % (filename, line_number))
else:
# Currently not supporting SSH1 style.
#m = ssh.keys.openssh_key_formats.ssh1_key.match(line)
continue
if changed is None:
return 0
else:
raise changed
def _match_host(self, host, pattern):
patterns = pattern.split(',')
# Negated_Pattern is used to terminate the checks.
try:
for p in patterns:
if self._match_pattern(host, p):
return 1
except OpenSSH_Known_Hosts.Negated_Pattern:
return 0
return 0
class Negated_Pattern(Exception):
pass
def _match_pattern(self, host, pattern):
# XXX: OpenSSH does not do any special work to check IP addresses.
# It just assumes that it will match character-for-character.
# Thus, 001.002.003.004 != 1.2.3.4 even though those are technically
# the same IP.
if pattern and pattern[0]=='!':
negate = 1
pattern = pattern[1:]
else:
negate = 0
if host == pattern:
if negate:
raise OpenSSH_Known_Hosts.Negated_Pattern
else:
return 1
# Check for wildcards.
# XXX: Lazy
# XXX: We could potentially escape other RE-special characters.
pattern = pattern.replace('.', '[.]')
# Convert * and ? wildcards into RE wildcards.
pattern = pattern.replace('*', '.*')
pattern = pattern.replace('?', '.')
pattern = pattern + '$'
r = re.compile(pattern, re.IGNORECASE)
if r.match(host):
if negate:
raise OpenSSH_Known_Hosts.Negated_Pattern
else:
return 1
else:
return 0
def _match_key(self, key_obj, base64_key):
key = key_obj.name + ' ' + base64_key
# XXX: static or class method would make this instantiation not necessary.
# Too bad the syntax sucks.
x = ssh.keys.openssh_key_storage.OpenSSH_Key_Storage()
parsed_key = x.parse_public_key(key)
if parsed_key.public_key == key_obj.public_key:
return 1
else:
return 0
def update_known_hosts(self, host, public_host_key, username=None):
# XXX: Locking
filename = self.get_users_known_hosts_filename(username)
tmp_filename = filename + '.tmp'
try:
f = open(filename)
except IOError, why:
if why.errno == errno.ENOENT:
f = None
else:
raise
f_tmp = open(tmp_filename, 'w')
# This is a flag used to indicate that we made the update.
# If, after parsing through the original known_hosts file, and we
# have not done the update, then we will just append the new key to
# the file.
updated = 0
if f:
for line in f.readlines():
line = line.strip()
new_line = line
if len(line)!=0 and line[0]!='#':
m = ssh.keys.openssh_key_formats.ssh2_known_hosts_entry.match(line)
if m:
if public_host_key.name == m.group('keytype'):
# Same keytype..See if we need to update.
# If the key is the same, then just update the host list.
# XXX: This code needs to be refactored.
base64_key = m.group('base64_key')
binary_key = binascii.a2b_base64(base64_key)
if public_host_key.name == 'ssh-dss':
key_obj = ssh.keys.dss.SSH_DSS()
elif public_host_key.name == 'ssh-rsa':
key_obj = ssh.keys.dss.SSH_RSA()
else:
# This should never happen.
raise ValueError, public_host_key.name
host_list = m.group('list_of_hosts')
host_list = host_list.split(',')
key_obj.set_public_key(binary_key)
if key_obj.get_public_key_blob() == public_host_key.get_public_key_blob():
# Same key.
# Add this host to the list if it is not already there.
tmp_host_list = [ x.lower() for x in host_list ]
if host.lower() not in tmp_host_list:
host_list.append(host)
comment = m.group('comment')
if comment is None:
comment = ''
new_line = ','.join(host_list) + ' ' + m.group('keytype') + ' ' + m.group('base64_key') + comment
updated = 1
else:
# Keys differ...Remove this host from the list if it was in there.
new_host_list = filter(lambda x,y=host.lower(): x.lower()!=y, host_list)
comment = m.group('comment')
if comment is None:
comment = ''
new_line = ','.join(new_host_list) + ' ' + m.group('keytype') + ' ' + m.group('base64_key') + comment
else:
# XXX: Support SSH1 keys.
pass
f_tmp.write(new_line + '\n')
if not updated:
# Append to the end.
base64_key = base64.encodestring(public_host_key.get_public_key_blob())
# Strip the newlines that the base64 module inserts.
base64_key = base64_key.replace('\n', '')
f_tmp.write(host + ' ' + public_host_key.name + ' ' + base64_key + '\n')
if f:
f.close()
f_tmp.close()
# XXX: Permissions??
os.rename(tmp_filename, filename)
import unittest
class openssh_known_hosts_test_case(unittest.TestCase):
pass
class check_for_host_test_case(openssh_known_hosts_test_case):
def runTest(self):
# Build a sample known_hosts test file.
tmp_filename = os.tempnam()
f = open(tmp_filename, 'w')
f.write("""# Example known hosts file.
10.1.1.108 ssh-dss AAAAB3NzaC1kc3MAAACBAOdTJwlIDyxIKAaCoGr/XsySV8AzJDU2fAePcO7CBURUYyUHS9uKsgsjZw7qBkdnkWT/Yx2Z8k9j+HuJ2L3mI6y9+cknen9ycze6g/UwmYa2+forEz7NXiEWi3lTHXnAXjEpmCWZAB6++HPM9rq+wQluOcN8cks57sttzxzqQG6RAAAAFQCgQ/edsYFn4jEBiJhoj97GElWM0QAAAIAz5hffnGq9zK5rtrNpKVdz6KIEyeXlGd16f6NwotVwpNd2zEXBC3+z13TyMiX35H9m7fWYQen7O7RNekJl5Gz7V6UA7lipNFrhmg/eO6rnXetrrgjdiHF5mSx3O8uBQOU5tK+IyAINtBhDqM6GNEqEkFa9yT6POYjGA8ihSaUUOQAAAIEAvvDYfg+KrBZUlJGMK/g1muBbBu5o+UbppgRTOsEfAMKRovV0vsZc4AIaeh/uGVKS+zXqQHh7btHgTMQ47hxF3tPVFWIgO6vDtsQX90e9xaCfmKQY2EV0Wrq1XUKxOycTNRZ5kCxYkq4qRhs5QnqB/Ov71g7HHxsJ8pnjSDusiNo=
172.16.1.11 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvUNY7kd1sDujt9HhdT6VWtf8yVRAw2Ib+M6ptWTuWWnPGR6TP/ZwumSs/rAguyxWrNRbw7Eainr/BTEFATpJRYKUDPZKGHLT3ixtOy7scUVRyaJD7F3L7BujkhHLWOyFJGtoZmJEdQmddGDwq+16gLD06GA8/N8kkQFRR6vwlRs=
64.70.20.70,64.70.44.3 1024 35 162807158017859311401243513535320968370503503816817576276599779420791975206320054411137858395244854129122865069311130487158120563446636918588974972115213166463069362091898230386572857193095086738994217228848073927343769936543295334648942676920084374567042307974866766193585693129128286570059425685457486987781
lists.ironport.com,10.1.1.109 ssh-dss AAAAB3NzaC1kc3MAAACBAOfO0s6KFDk8lU7hJyLWevjEIi9drfn8wJYFvYAc+apN4+Qlq4DtFXMDH8U5pQWpZsj705ywi5cex8aEaeepfeQBe6NQCmJci47cTTnaiy/IR7d2hZkB0LmJJX6JxYWWtk2kFyL4xbPEfXbBpNprTfNzgi32YeeIKak3T3amYo8dAAAAFQDInSP36WJZ7WnH13qBXZM+5USftwAAAIAQ7CHz/hwxpmYNind6Zm7vmFC8JkRdkTjNIfyuHszfgHI3+imhSJJxjaSwdvLNi+2P2cdoTrL45ITPFT0+YSq1VIXclqa0k0kjETFbayGbq9DE3w7S6WBiiewTcllu7NzO9EvaNt3XJUQ7SpvNBoLhv+XAHkdhX0ouwwtyeElT6gAAAIAEcOOSQClq9CIcYEjwtfDBANaJ7o2WfYJMqto+ibjnl+1YFtGw9ofD5gi5gtEIGtSb6mO88ooX9sfmkaAY+1L/gTdb3Fxc3zuL2PymBt1ruNTgVzEjV35h94lgC3+F4mPz0jQpnpsbxhm/uDn/i1BeRBlzMhyWOAHfLknna9WCmg==
!outlaw.qa,*.qa,172.17.0.201 ssh-dss AAAAB3NzaC1kc3MAAACBAPaQAeia7kiuORu9425IyZRKlRPkom9mjEVERjN3Lw5R93rBZSwbl8wiT1PEeBN2047SZD7ucHaAUqAU39l//JVA0Q/RHXczad1niqC7Y7YKSpu3XfI7vpgMd91XIlxNhnhvNLtWfmwuWuX1FFiByKUY7fsHVeKTYwnvRPiv89IBAAAAFQCryy7v2z5Olv1Z0bSoQLDemiSzywAAAIEA3pmx1n0YRuw3hY4RfXbQCUxtu19bldG4XlNnmeIE8cb4tdGHBgLnLrMpSLsA4aMOWAzzDvB/Gk9AlgyNuYp2NaCFStE5yYiK9c+wTNpChCsDx/BqWMtPYTKQDZmhmSp94noIQd429OIJhQt1qL/7vHD1Tac/2V33TsADYW4aS+4AAACBANC4tVdIkB5vyLm2BrjK+P7uS8SaUSfKaAd83XahVz2q8cIeiFHrXvfLRFeks99vgxSPq6mqxC5zpcDGFWBm1UJY4PxyG+t6AhgYEPefD+ofXAvTHLPIRJbNv2BDP6vHOKRfAYtWGbQf6sXw4VwS9mAR6JHlGoHMLnRewMcq49jE
*.com,test04.god ssh-dss AAAAB3NzaC1kc3MAAACBAK3p8k1i9I/m0no3LAS4etFGsommDJcBQfsuP/nn42O0VyDXltcfjLvWxABZow6iKJHiHZ8FN/FxOX+jZUlIplrs6oRYbKeWegq3NcvelEderWhIyOKrDZHgO9HprwamSMWFxDG5kUSJ/em/G5N+rGv8K7dJfCus42ynh0+a/Q1dAAAAFQD1/X/izKQrZs//Q5HgVVOfEqK6+wAAAIBQw1TWAHQHiihsCbbMbGuzm/7Rq9YTvGNyzmBgAP/fbmv/Vi3lZwmTilKSkebEFvrWeAT1hI9KufzjeRhkUCZGzCmCt7A614/brJRIznOAvWaTRsy/wzw7kdARljdQRTcnSXnpc81jEzMyt2SzcifZOvyNfIhAtFXX6yXeFg1dpgAAAIBoJZa1MTGEWJ43BcFftRGbnf/EK5+SDlYgrSiJZeGAUURvrdJPPtCSRtQU7ldiGfKiPcD/6U0XcC9o09/sDSfFOEtTFnawe74pqcQVT3x2hQ5Zs1W82M2arNXaoYBo21RAE4oy1u010a4hjxPoSrAVyQXVwL2Sv8B5vDu99sIu1w==
""")
f.close()
# Make a subclass so we can control which file it loads.
class custom_known_hosts(OpenSSH_Known_Hosts):
def __init__(self, tmp_filename):
self.tmp_filename = tmp_filename
def get_known_hosts_filenames(self, username):
return [self.tmp_filename]
try:
import openssh_key_storage
keystore = openssh_key_storage.OpenSSH_Key_Storage()
x = custom_known_hosts(tmp_filename)
# Make some keys to test against.
# 10.1.1.108
k1 = keystore.parse_public_key('ssh-dss AAAAB3NzaC1kc3MAAACBAOdTJwlIDyxIKAaCoGr/XsySV8AzJDU2fAePcO7CBURUYyUHS9uKsgsjZw7qBkdnkWT/Yx2Z8k9j+HuJ2L3mI6y9+cknen9ycze6g/UwmYa2+forEz7NXiEWi3lTHXnAXjEpmCWZAB6++HPM9rq+wQluOcN8cks57sttzxzqQG6RAAAAFQCgQ/edsYFn4jEBiJhoj97GElWM0QAAAIAz5hffnGq9zK5rtrNpKVdz6KIEyeXlGd16f6NwotVwpNd2zEXBC3+z13TyMiX35H9m7fWYQen7O7RNekJl5Gz7V6UA7lipNFrhmg/eO6rnXetrrgjdiHF5mSx3O8uBQOU5tK+IyAINtBhDqM6GNEqEkFa9yT6POYjGA8ihSaUUOQAAAIEAvvDYfg+KrBZUlJGMK/g1muBbBu5o+UbppgRTOsEfAMKRovV0vsZc4AIaeh/uGVKS+zXqQHh7btHgTMQ47hxF3tPVFWIgO6vDtsQX90e9xaCfmKQY2EV0Wrq1XUKxOycTNRZ5kCxYkq4qRhs5QnqB/Ov71g7HHxsJ8pnjSDusiNo=')
# lists.ironport.com
k2 = keystore.parse_public_key('ssh-dss AAAAB3NzaC1kc3MAAACBAOfO0s6KFDk8lU7hJyLWevjEIi9drfn8wJYFvYAc+apN4+Qlq4DtFXMDH8U5pQWpZsj705ywi5cex8aEaeepfeQBe6NQCmJci47cTTnaiy/IR7d2hZkB0LmJJX6JxYWWtk2kFyL4xbPEfXbBpNprTfNzgi32YeeIKak3T3amYo8dAAAAFQDInSP36WJZ7WnH13qBXZM+5USftwAAAIAQ7CHz/hwxpmYNind6Zm7vmFC8JkRdkTjNIfyuHszfgHI3+imhSJJxjaSwdvLNi+2P2cdoTrL45ITPFT0+YSq1VIXclqa0k0kjETFbayGbq9DE3w7S6WBiiewTcllu7NzO9EvaNt3XJUQ7SpvNBoLhv+XAHkdhX0ouwwtyeElT6gAAAIAEcOOSQClq9CIcYEjwtfDBANaJ7o2WfYJMqto+ibjnl+1YFtGw9ofD5gi5gtEIGtSb6mO88ooX9sfmkaAY+1L/gTdb3Fxc3zuL2PymBt1ruNTgVzEjV35h94lgC3+F4mPz0jQpnpsbxhm/uDn/i1BeRBlzMhyWOAHfLknna9WCmg==')
# 172.17.0.201
k3 = keystore.parse_public_key('ssh-dss AAAAB3NzaC1kc3MAAACBAPaQAeia7kiuORu9425IyZRKlRPkom9mjEVERjN3Lw5R93rBZSwbl8wiT1PEeBN2047SZD7ucHaAUqAU39l//JVA0Q/RHXczad1niqC7Y7YKSpu3XfI7vpgMd91XIlxNhnhvNLtWfmwuWuX1FFiByKUY7fsHVeKTYwnvRPiv89IBAAAAFQCryy7v2z5Olv1Z0bSoQLDemiSzywAAAIEA3pmx1n0YRuw3hY4RfXbQCUxtu19bldG4XlNnmeIE8cb4tdGHBgLnLrMpSLsA4aMOWAzzDvB/Gk9AlgyNuYp2NaCFStE5yYiK9c+wTNpChCsDx/BqWMtPYTKQDZmhmSp94noIQd429OIJhQt1qL/7vHD1Tac/2V33TsADYW4aS+4AAACBANC4tVdIkB5vyLm2BrjK+P7uS8SaUSfKaAd83XahVz2q8cIeiFHrXvfLRFeks99vgxSPq6mqxC5zpcDGFWBm1UJY4PxyG+t6AhgYEPefD+ofXAvTHLPIRJbNv2BDP6vHOKRfAYtWGbQf6sXw4VwS9mAR6JHlGoHMLnRewMcq49jE')
# test04.god
k4 = keystore.parse_public_key('ssh-dss AAAAB3NzaC1kc3MAAACBAK3p8k1i9I/m0no3LAS4etFGsommDJcBQfsuP/nn42O0VyDXltcfjLvWxABZow6iKJHiHZ8FN/FxOX+jZUlIplrs6oRYbKeWegq3NcvelEderWhIyOKrDZHgO9HprwamSMWFxDG5kUSJ/em/G5N+rGv8K7dJfCus42ynh0+a/Q1dAAAAFQD1/X/izKQrZs//Q5HgVVOfEqK6+wAAAIBQw1TWAHQHiihsCbbMbGuzm/7Rq9YTvGNyzmBgAP/fbmv/Vi3lZwmTilKSkebEFvrWeAT1hI9KufzjeRhkUCZGzCmCt7A614/brJRIznOAvWaTRsy/wzw7kdARljdQRTcnSXnpc81jEzMyt2SzcifZOvyNfIhAtFXX6yXeFg1dpgAAAIBoJZa1MTGEWJ43BcFftRGbnf/EK5+SDlYgrSiJZeGAUURvrdJPPtCSRtQU7ldiGfKiPcD/6U0XcC9o09/sDSfFOEtTFnawe74pqcQVT3x2hQ5Zs1W82M2arNXaoYBo21RAE4oy1u010a4hjxPoSrAVyQXVwL2Sv8B5vDu99sIu1w==')
# Make a key that doesn't exist in the known hosts file.
unknown_key = keystore.parse_public_key('ssh-dss AAAAB3NzaC1kc3MAAACBAJSc17NO4rxvhUfwjMzJMG9On9umzzlbwlN0wBv5riYetE1flTyySOUPa8YvpNYmMs5GSz0CzO/FI/EM/rgYvpvA+KKpV/9oL+XoT/O36t6Q8MZIGXwj75lxP8X9NSZxO0b5E7CRDyW5rsl6xfa3YaQrWqZRKhOeGASWRYtUZcpVAAAAFQCazkzFpIwqEpAbn0jZlkUHKbpwuQAAAIA4AGcL/OMIDtxC7T1smSPVk0VEr5i+IfL4xPLRSQCw6/Jr4OLzBH/TiTAjyp7NZszIu586J85t1nO3kOx/fKI8Ik2jDvJOmdUtDvMZnbZK1rvFiw3dCxEERGVW1LjyAnxtebl/pOJ6CpO4Pfh87mx+iH9m90oZSCDz602DXUz50wAAAIA0mmctzgavC8ApEsbKI69MhaYhkyxvEaucTarkGPAPvXPurfVJ8ZwtK3dYckLgn3a5WPHWqIZVfmtSbnkwld+t3BIl8IX6bKa2WaffUeU6k50ssUV6IvW+IHd0JJ/mwE6f9caNS7x0pC0+DQujp553IP5cr9NskQTK4j/Iwwlkrw==')
# 172.16.1.11
k5 = keystore.parse_public_key('ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvUNY7kd1sDujt9HhdT6VWtf8yVRAw2Ib+M6ptWTuWWnPGR6TP/ZwumSs/rAguyxWrNRbw7Eainr/BTEFATpJRYKUDPZKGHLT3ixtOy7scUVRyaJD7F3L7BujkhHLWOyFJGtoZmJEdQmddGDwq+16gLD06GA8/N8kkQFRR6vwlRs=')
# Do the tests.
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('10.1.1.108',''), k1), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('1.2.3.4',''), k1), 0)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('0.0.0.0','lists.ironport.com'), k2), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('lists.ironport.com', '10.1.1.109'), k2), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('10.1.1.109',''), k2), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('0.0.0.0','outlaw.qa'), k3), 0)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('0.0.0.0','foo.qa'), k3), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('172.17.0.201',''), k3), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('0.0.0.0','foo.com'), k4), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('0.0.0.0','test04.god'), k4), 1)
self.assertRaises(Host_Key_Changed_Error, x.check_for_host, IPv4_Remote_Host_ID('10.1.1.108',''), k2)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('lists.ironport.com', '10.1.1.108'), k1), 1)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('0.0.0.0','unknown.dom'), k1), 0)
self.assertRaises(Host_Key_Changed_Error, x.check_for_host, IPv4_Remote_Host_ID('10.1.1.108',''), unknown_key)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('172.16.1.11',''), unknown_key), 0)
self.assertEqual(x.check_for_host(IPv4_Remote_Host_ID('172.16.1.11',''), k5), 1)
finally:
os.unlink(tmp_filename)
def suite():
suite = unittest.TestSuite()
suite.addTest(check_for_host_test_case())
return suite
if __name__ == '__main__':
unittest.main(defaultTest='suite', module='openssh_known_hosts')
# $Header: //prod/main/ap/ssh/ssh/keys/public_private_key.py#1 $
"""ssh.keys.public_private_key
This is the base public/private key object.
Specific key types subclass this for their implementation.
"""
__version__ = '$Revision: #1 $'
import hashlib
class SSH_Public_Private_Key:
"""SSH_Public_Private_Key
Base class for any type of public/private key.
"""
name = 'none'
# Features of this key type.
supports_signature = 0
supports_encryption = 0
# Keys are encoded according to the implementation.
private_key = None
public_key = None
def set_public_key(self, public_key):
"""set_public_key(self, public_key) -> None
Sets the public key. public_key is a string encoded according
to the algorithm.
"""
raise NotImplementedError
def set_private_key(self, private_key):
"""set_private_key(self, private_key) -> None
Sets the private key. private_key is a string encoded according
to the algorithm.
"""
raise NotImplementedError
def get_public_key_blob(self):
raise NotImplementedError
def get_private_key_blob(self):
raise NotImplementedError
def sign(self, message):
"""sign(self, message, ) -> signature
Signs a message with the given private_key.
<message> is a string of bytes.
The resulting signature is encoded as a payload of (string, bytes)
where string is the signature format identifier and byes is the
signature blob.
"""
raise NotImplementedError
def verify(self, message, signature):
"""verify(self, message, signature, public_key) -> boolean
Returns true or false if the signature is a match for the
signature of <message>.
<message> is a string of bytes.
<signature> is a payload encoded as (string, bytes).
"""
raise NotImplementedError
def public_key_fingerprint(self):
"""public_key_fingerprint(self) -> fingerprint string
Returns a fingerprint of the public key.
"""
m = hashlib.md5(self.get_public_key_blob())
# hexdigest returns lowercase already, but I just wanted to be careful.
fingerprint = m.hexdigest().lower()
pieces = [ fingerprint[x]+fingerprint[x+1] for x in xrange(0, len(fingerprint), 2) ]
return ':'.join(pieces)
# XXX: encrypt functions...
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# remote_host
#
# This module is the class that abstracts the ID of a host.
# Typically the ID is based on the IP or hostname of the remote host, but this
# allows you to use non-IP configurations.
#
class Remote_Host_ID:
pass
class IPv4_Remote_Host_ID(Remote_Host_ID):
"""IPv4_Remote_Host_ID
Represents the ID of the remote host.
<ip> is required.
<hostname> is optional.
"""
ip = ''
hostname = None
def __init__(self, ip, hostname):
self.ip = ip
self.hostname = hostname
def __repr__(self):
return '<IPv4_Remote_Host_ID instance ip=%r hostname=%r>' % (self.ip, self.hostname)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.rsa
#
# Encapsulates the RSA key.
__version__ = '$Revision: #1 $'
import hashlib
import public_private_key
import ssh.util.packet
import ssh.util.random
from Crypto.PublicKey import RSA
from Crypto.Util import number
# This is the DER encoding of the SHA1 identifier.
SHA1_Digest_Info = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
class SSH_RSA(public_private_key.SSH_Public_Private_Key):
# Features of this key type.
supports_signature = 1
supports_encryption = 0
name = 'ssh-rsa'
private_key = (0L, 0L, 0L, 0L, 0L) # n, e, d, p, q
public_key = (0L, 0L) # e, n
def set_public_key(self, public_key):
rsa, e, n = ssh.util.packet.unpack_payload(RSA_PUBLIC_KEY_PAYLOAD, public_key)
if rsa != 'ssh-rsa':
raise ValueError, rsa
self.public_key = (e, n)
def set_private_key(self, private_key):
rsa, n, e, d, p, q = ssh.util.packet.unpack_payload(RSA_PRIVATE_KEY_PAYLOAD, private_key)
if rsa != 'ssh-rsa':
raise ValueError, rsa
self.public_key = (n, e, d, p, q)
def get_public_key_blob(self):
e, n = self.public_key
return ssh.util.packet.pack_payload(RSA_PUBLIC_KEY_PAYLOAD,
('ssh-rsa',
e, n))
def get_private_key_blob(self):
n, e, d, p, q = self.public_key
return ssh.util.packet.pack_payload(RSA_PRIVATE_KEY_PAYLOAD,
('ssh-rsa',
n, e, d, p, q))
def emsa_pkcs1_v1_5_encode(self, message, n_len):
"""emsa_pkcs1_v1_5_encode(self, message, n_len) -> encoded_message
Encodes the given string via the EMSA PKCS#1 version 1.5 method.
<message> - The string to encode.
<n_len> - The length (in octets) of the RSA modulus n.
"""
hash = hashlib.sha1(message).digest()
T = SHA1_Digest_Info + hash
if __debug__:
assert n_len >= len(T)+11
# PKCS spec says that it's -3...I do not understand why that doesn't work.
PS = '\xff'*(n_len - len(T) - 2)
if __debug__:
assert len(PS) >= 8
return '\x00\x01' + PS + '\x00' + T
def sign(self, message):
n, e, d, p, q = self.private_key
rsa_obj = RSA.construct( (n, e, d, p, q) )
modulus_n_length_in_octets = rsa_obj.size()/8
encoded_message = self.emsa_pkcs1_v1_5_encode(message, modulus_n_length_in_octets)
signature = rsa_obj.sign(encoded_message, '')[0] # Returns tuple of 1 element.
signature = number.long_to_bytes(signature)
return ssh.util.packet.pack_payload(RSA_SIG_PAYLOAD,
('ssh-rsa',
signature))
def verify(self, message, signature):
e, n = self.public_key
rsa, blob = ssh.util.packet.unpack_payload(RSA_SIG_PAYLOAD, signature)
if rsa != 'ssh-rsa':
raise ValueError, rsa
s = number.bytes_to_long(blob)
rsa_obj = RSA.construct( (n, e) )
modulus_n_length_in_octets = rsa_obj.size()/8
encoded_message = self.emsa_pkcs1_v1_5_encode(message, modulus_n_length_in_octets)
return rsa_obj.verify(encoded_message, (s,))
RSA_PUBLIC_KEY_PAYLOAD = (ssh.util.packet.STRING, # "ssh-rsa"
ssh.util.packet.MPINT, # e
ssh.util.packet.MPINT # n
)
RSA_PRIVATE_KEY_PAYLOAD = (ssh.util.packet.STRING, # "ssh-rsa"
ssh.util.packet.MPINT, # n
ssh.util.packet.MPINT, # e
ssh.util.packet.MPINT, # d
ssh.util.packet.MPINT, # p
ssh.util.packet.MPINT, # q
)
RSA_SIG_PAYLOAD = (ssh.util.packet.STRING, # "ssh-rsa"
ssh.util.packet.STRING # signature_key_blob
)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.keys.static_key_storage
#
# This module is a key storage type where they keys are retained in memory.
#
__version__ = '$Revision: #1 $'
import os
import key_storage
import remote_host
import openssh_key_storage
class Static_Key_Storage(key_storage.SSH_Key_Storage):
"""Static_Key_Storage
A key storage mechanism where all keys are maintained in memory.
Note that this isn't terribly secure.
"""
key_types = ('dsa', 'rsa')
def __init__(self):
# The key is the username, the value is the key object.
self.public_key = {}
self.private_key = {}
# List of (hosts, (SSH_Public_Private_Key,...)) where hosts is a list of strings.
# Does not support any fancy meta-matching, sorry.
# Does not support user-specific known hosts.
self.known_hosts = []
def set_private_host_key(self, username, key_obj):
self.private_key[username] = key_obj
def set_public_host_key(self, username, key_obj):
self.public_key[username] = key_obj
def add_known_hosts(self, hosts, keystrings):
"""add_known_hosts(self, hosts, keystrings) -> None
Add a known host.
<hosts>: List of host strings.
<keystrings>: List of strings of the host's public key.
Must be in OpenSSH's standard format.
"""
key_storage = openssh_key_storage.OpenSSH_Key_Storage
key_objs = []
for keystring in keystrings:
key_obj = key_storage.parse_public_key(keystring)
key_objs.append(key_obj)
# XXX: Could merge host entries.
self.known_hosts.append((hosts, key_objs))
def load_keys(self, username=None):
if not self.public_key.has_key(username) or not self.private_key.has_key(username):
return None
if username is None:
username = os.getlogin()
key_obj = self.private_key[username]
public_key = self.public_key[username]
key_obj.public_key = public_key.public_key
return [key_obj]
load_keys.__doc__ = key_storage.SSH_Key_Storage.load_keys.__doc__
def load_private_keys(self, username=None):
if not self.private_key.has_key(username):
return []
if username is None:
username = os.getlogin()
return [self.private_key[username]]
load_private_keys.__doc__ = key_storage.SSH_Key_Storage.load_private_keys.__doc__
def load_public_keys(self, username=None):
if not self.public_key.has_key(username):
return []
if username is None:
username = os.getlogin()
return [self.public_key[username]]
load_public_keys.__doc__ = key_storage.SSH_Key_Storage.load_public_keys.__doc__
def verify(self, host_id, server_key_types, public_host_key, username=None):
if username is None:
username = os.getlogin()
for key in server_key_types:
if public_host_key.name == key.name:
# This is a supported key type.
if self._verify_contains(host_id, public_host_key, username):
return 1
return 0
verify.__doc__ = key_storage.SSH_Key_Storage.verify.__doc__
def _verify_contains(self, host_id, public_host_key, username):
__pychecker__ = 'unusednames=username'
# Currently only supported IPv4
if not isinstance(host_id, remote_host.IPv4_Remote_Host_ID):
return 0
for hosts, key_objs in self.known_hosts:
for key_obj in key_objs:
if key_obj.name == public_host_key.name:
for host in hosts:
if host == host_id.ip or host == host_id.hostname:
if key_obj.public_key == public_host_key.public_key:
return 1
return 0
def update_known_hosts(self, host, public_host_key, username=None):
__pychecker__ = 'unusednames=username'
self.known_hosts.append(([host], public_host_key))
update_known_hosts.__doc__ = key_storage.SSH_Key_Storage.update_known_hosts.__doc__
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.l4_transport
#
# Base transport class to implement networking.
#
__version__ = '$Revision: #1 $'
class Transport:
def connect(self):
raise NotImplementedError
def read(self, bytes):
raise NotImplementedError
def write(self, data):
raise NotImplementedError
def read_line(self):
raise NotImplementedError
def close(self):
raise NotImplementedError
def get_host_id(self):
"""get_host_id(self) -> Remote_Host_ID instance
Get's the Remote_Host_ID instance for the other side
of this connection.
"""
raise NotImplementedError
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.l4_transport.coro_socket_transport
#
# Socket transport used by SSH.
#
__version__ = '$Revision: #2 $'
import coro
import dnsqr
import dns_exceptions
import errno
import inet_utils
import ironutil
import socket
import ssh.l4_transport
import ssh.keys.remote_host
class coro_socket_transport(ssh.l4_transport.Transport):
# The socket
s = None
def __init__(self, ip, port=22, bind_ip=None, hostname=None):
assert inet_utils.is_ip(ip)
self.ip = ip
self.port = port
self.bind_ip = bind_ip
self.hostname = hostname
self.s = coro.make_socket(socket.AF_INET, socket.SOCK_STREAM)
def connect(self):
if self.bind_ip is not None:
self.s.bind((self.bind_ip, 0))
self.s.connect((self.ip, self.port))
def read(self, bytes):
# XXX: This could be made more efficient.
count = bytes
result = []
while count > 0:
try:
chunk = self.s.recv(count)
except OSError, why:
if why.errno == errno.EBADF:
raise EOFError
else:
raise
except coro.ClosedError:
raise EOFError
if len(chunk)==0:
raise EOFError
count -= len(chunk)
result.append(chunk)
return ''.join(result)
def write(self, bytes):
try:
return self.s.send(bytes)
except OSError, why:
if why.errno == errno.EBADF:
ironutil.raise_oserror(errno.EPIPE)
else:
raise
except coro.ClosedError:
ironutil.raise_oserror(errno.EPIPE)
def read_line(self):
# XXX: This should be made more efficient with buffering.
# However, the complexity and overhead of adding buffering just
# to support reading the line at the beginning of the protocol
# negotiation seems kinda silly.
result = []
while 1:
try:
it = self.s.recv(1)
except OSError, why:
if why.errno == errno.EBADF:
raise EOFError
else:
raise
except coro.ClosedError:
raise EOFError
if not it:
raise EOFError
if it == '\r':
# This is a part of CR LF line ending. Skip it.
pass
elif it == '\n':
break
else:
result.append(it)
return ''.join(result)
def close(self):
self.s.close()
def get_hostname(self):
if self.hostname is None:
try:
in_addr = inet_utils.to_in_addr(self.ip)
self.hostname = dnsqr.query (in_addr, 'PTR')[0][1]
except (dns_exceptions.DNS_Error, IndexError):
# XXX: Log debug message.
pass
return self.hostname
def get_host_id(self):
return ssh.keys.remote_host.IPv4_Remote_Host_ID(self.ip, self.get_hostname())
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.mac
#
# This implements the interface to support message authentication codes.
#
__version__ = '$Revision: #1 $'
class SSH_MAC_Method:
"""SSH_MAC_Method
Base class for any type of Message Authentication Code alogirthm.
"""
name = ''
block_size = 1
digest_size = 0
key_size = 0
key = None
def digest(self, sequence_number, data):
raise NotImplementedError
def set_key(self, key):
self.key = key
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_hmac
#
# This implements the base HMAC hashing algorithm from RFC 2104.
#
__version__ = '$Revision: #1 $'
import ssh.mac
import ssh.util
import struct
class SSH_HMAC(ssh.mac.SSH_MAC_Method):
"""SSH_HMac
Base class of other HMAC algorithms.
See RFC 2104.
"""
def get_hash_object(self):
raise NotImplementedError
def set_key(self, key):
if __debug__:
# Insecure.
assert len(key) >= self.digest_size
if len(key) > self.block_size:
# Key is too big. Hash it and use the result as the key.
# This really isn't necessary because we're certain that the key
# is going to be the correct size. However, I put it in here
# for completeness with the HMAC spec.
import sys
sys.stderr.write('WARNING: Unexecpted HMAC key size!!!\n')
h = self.get_hash_object()
self.key = h.update(key).digest()
else:
self.key = key
ipad = '\x36' * self.block_size
opad = '\x5C' * self.block_size
padded_key = self.key + '\0' * (self.block_size-len(self.key))
self._enc_ipad = ssh.util.str_xor(padded_key, ipad)
self._enc_opad = ssh.util.str_xor(padded_key, opad)
def digest(self, sequence_number, data):
sequence_number = struct.pack('>L', sequence_number)
return self.hmac(sequence_number + data)
def hmac(self, data):
# H(K XOR opad, H(K XOR ipad, text))
hash = self.get_hash_object()
hash.update(self._enc_ipad)
hash.update(data)
b = hash.digest()
hash = self.get_hash_object()
hash.update(self._enc_opad)
hash.update(b)
return hash.digest()
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_hmac_md5
#
# Implements the hmac-md5 SSH MAC algorithm.
__version__ = '$Revision: #1 $'
from hmac import SSH_HMAC
import hashlib
class HMAC_MD5(SSH_HMAC):
name = 'hmac-md5'
block_size = 64
digest_size = 16
key_size = 16
def get_hash_object(self):
return hashlib.md5()
import unittest
class ssh_hmac_md5_test_case(unittest.TestCase):
pass
class hmac_md5_test_case(ssh_hmac_md5_test_case):
def runTest(self):
# From RFC2104
a = HMAC_MD5()
a.set_key('\x0b'*16)
self.assertEqual(a.hmac('Hi There'), '\x92\x94\x72\x7a\x36\x38\xbb\x1c\x13\xf4\x8e\xf8\x15\x8b\xfc\x9d')
a = HMAC_MD5()
a.set_key('Jefe' + '\0'*12)
self.assertEqual(a.hmac('what do ya want for nothing?'), '\x75\x0c\x78\x3e\x6a\xb0\xb5\x03\xea\xa8\x6e\x31\x0a\x5d\xb7\x38')
a = HMAC_MD5()
a.set_key('\xAA'*16)
self.assertEqual(a.hmac('\xDD' * 50), '\x56\xbe\x34\x52\x1d\x14\x4c\x88\xdb\xb8\xc7\x33\xf0\xe8\xb3\xf6')
def suite():
suite = unittest.TestSuite()
suite.addTest(hmac_md5_test_case())
return suite
if __name__ == '__main__':
unittest.main(defaultTest='suite')
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_hmac_sha1
#
# Implements the hmac-sha1 SSH MAC algorithm.
__version__ = '$Revision: #1 $'
from hmac import SSH_HMAC
import hashlib
class HMAC_SHA1(SSH_HMAC):
name = 'hmac-sha1'
block_size = 64
digest_size = 20
key_size = 20
def get_hash_object(self):
return hashlib.sha1()
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.mac.none
#
# This is the 'none' mac that returns an empty value.
#
__version__ = '$Revision: #1 $'
import ssh.mac
class MAC_None(ssh.mac.SSH_MAC_Method):
name = 'none'
def digest(self, sequence_number, data):
return ''
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Secure Copy.
Overview
========
This implements the scp "protocol" for transferring files over SSH.
AFAIK, the SCP/RCP "protocol" is not documented anywhere. This implemented is
inferred from the BSD and OpenSSH source code.
Options
=======
Running the SCP process from the command line has the options defined in the
usage string in `ssh.scp.cli`.
The scp command connects to the remote host and runs the "scp" process
(assuming it is in your PATH somewhere).
If it is sending the file to the remote host, then it has the following
options:
scp [options] -t pathnames...
If it is getting a file from the remote host, then it has the following
options:
scp [options] -f target
A special option for running in remote mode is '-d'. This indicates that the
target should be a directory. This is only true if more than one source file
is specified.
The only other options supported by this implementation are "-p", "-r" and
"-v" which are user options defined in the usage string.
These options are not very consistent. For example, rcp in FreeBSD does not
support "-v", but it does support "-x" (which is for encryption and thus not
used in scp).
Messages
========
The SCP protocol involves one side (the client) sending commands to the other
side (the server). The commands are 1 letter, options for that command,
terminated by a newline character. The commands are:
- ``T``: Indicates the timestamp of the next file (only). The format is::
mtime_sec SPACE mtime_usec SPACE atime_sec SPACE atime_usec
The timestamps are POSIX timestamps in ASCII base-10. Most
implementations use 0 for the micro-second portions.
``mtime`` is the modified time. ``atime`` is the last access time.
This is only sent if the -p option is specified.
- ``C``: A file. The format is::
mode SPACE size SPACE filename
``mode`` is a 4-digit octal number in ASCII that is the mode of the file.
``size`` is a number in ASCII base-10 that indicates the size of the data
to follow.
``filename`` is the name of the file (with no path information).
The data immediately follows this entry.
- ``D``: A directory. The format is the same as a file (C), but the size
is zero. All following files will be in this specified directory.
- ``E``: The end of transmission. This has no options.
Care should be taken to make sure a filename does not contain a newline
character.
Responses
=========
Every command requires a response. A response is a message, with the following
codes:
- ``\0``: A successful response.
- ``\01``: An soft error occurred. The error string follows up to the
newline character. This does not stop the data stream.
- ``\02``: A hard error occurred. The error string follows up to the
newline character. This causes the receiving side (the server) to exit.
Any other character in a response is interpreted as a hard error.
Sending and receiving files has the following message/response timeline:
================= =================
Client Server
================= =================
Send C command
Wait for response
Send response
Got response
Send file
Receive file
Wait for response
Send response
Wait for response
Got response
Send response
Got response
================= =================
Transfer Modes
==============
There are various transfer modes that the scp program can run in. The
following is a description of those modes from the perspective of the person
initiating the scp command.
1. Local to local. This doesn't actually use SSH to transfer, and acts very
similar to cp.
2. Local to remote. The local side connects to the remote host with the -t
option and sends the file(s).
3. Remote to local. The local side connects to the remote host with the -f
option and requests file(s).
4. Remote to remote. The local side connects to the source remote host with
the -f option and connects to the target remote host with the -t option and
relays the data between the two hosts.
"""
__version__ = '$Revision: #1 $'
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""CLI interface.
This is the interface for running SCP and a command-line process.
"""
__version__ = '$Revision: #1 $'
import optparse
import sys
import ssh.scp.client
import ssh.scp.core
usage = """\
scp [options] file1 file2
scp [options] file1 ... directory
Each file or directory argument is either:
- A remote filename or directory in the format of
``username@hostname:pathname`` or ``hostname:pathname``.
- A local filename or directory. A local filename can not have any colons
before a slash.
If more than one source file is specified, the destination must be a directory
that exists.
The following options are available:
-p Preserve modification time, access time, and mode of the original file.
-r If any source filename is a directory, then transfer the directory and
all subfiles and directories to the remote host. The destination must
be a directory.
-v Produce verbose output. Up to 3 -v commands for more verbosity.
Note on file ownership and mode: If the destination is a file, and that file
already exists, then the mode and owner for the destination file is preserved.
If the destination is a directory or a filename that does not exist, then the
file will be created with the ownership of the remote user and the mode of the
original file modified by the umask of the remote user.
"""
class CLI:
def main(self, args=None):
"""The main entry point for the SCP cli program.
Calls sys.exit when finished.
:Parameters:
- `args`: The command line arguments as a list of strings. If
None, will use sys.argv.
"""
parser = optparse.OptionParser(usage=usage, add_help_option=False)
parser.add_option('-p', dest='preserve', action='store_true')
parser.add_option('-r', dest='recursive', action='store_true')
parser.add_option('-v', dest='verbosity', action='count')
parser.add_option('-t', dest='action_to', action='store_true')
parser.add_option('-f', dest='action_from', action='store_true')
parser.add_option('-d', dest='target_should_be_dir', action='store_true')
parser.add_option('--help', dest='help', action='store_true')
if args is None:
args = sys.argv[1:]
options, arguments = parser.parse_args(args)
if options.help:
print usage
sys.exit(0)
if options.action_from:
scp = self._get_scp()
scp.verbosity = options.verbosity
scp.debug(ssh.scp.core.DEBUG_EXTRA, 'options: %r', args)
scp.read_response()
scp.send(options.preserve,
options.recursive,
arguments
)
sys.exit(int(scp.had_errors))
elif options.action_to:
scp = self._get_scp()
scp.verbosity = options.verbosity
scp.debug(ssh.scp.core.DEBUG_EXTRA, 'options: %r', args)
if len(arguments) != 1:
scp.hard_error('Must specify 1 target.')
scp.receive(options.preserve,
options.recursive,
options.target_should_be_dir,
arguments[0]
)
sys.exit(int(scp.had_errors))
else:
print 'Function unavailable.'
sys.exit(1)
client = self._get_client()
client.main(options.preserve,
options.recursive,
options.verbosity,
arguments
)
def _get_scp(self):
return ssh.scp.core.SCP()
def _get_client(self):
return ssh.scp.client.Client()
if __name__ == '__main__':
cli = CLI()
cli.main()
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""SCP client.
This is the client side of the SCP process. It uses the SSH library to spawn
connections.
"""
__version__ = '$Revision: #1 $'
import ssh.scp.core
class Client(ssh.scp.core.SCP):
def main(self, preserve, recursive, verbose, pathnames):
raise NotImplementedError
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Core SCP code.
This implements the core code of the SCP program.
"""
__version__ = '$Revision: #1 $'
import glob
import os
import stat
import sys
import time
DEBUG_NORMAL = 1
DEBUG_EXTRA = 2
DEBUG_FULL = 3
class RootEscapeError(Exception):
"""Raised when a user tries to access files outside of the root directory."""
class SCP:
"""Core SCP implementation.
This implements the core transfer functions for SCP. It does not involve
any of the SSH code, so it is more similar to RCP.
:IVariables:
- `output`: A file-like object for sending data.
- `input`: A file-like object for receiving data.
- `verbosity`: Level of verbosity.
- `had_errors`: Flag to keep track if there were any errors.
- `blocksize`: Size of blocks to read and write.
- `path_filter`: A function to filter pathnames to determine whether or
not they are accessible. It should take one parameter, the full
pathname, and return True if it is ok, otherwise False. May be None
to indicate no filter.
- `filter_recursive_only`: If True, will only apply the filter for
recursive sends. The filter is always appled for receiving.
- `root`: The root of the filesystem. By setting this, you can
transparently reset the root (similar to chroot). Setting this will
implicitly change the current working directory to this directory.
May be None to indicate normal operation.
- `root_slash`: Same as ``root`` with a trailing slash.
- `shell_globbing`: If true, will do shell-style globbing on the file
arguments when sending files.
"""
output = None
input = None
had_errors = False
verbosity = 0
blocksize = 4096
max_line_size = 4096
root = None
root_slash = None
def __init__(self, input=None,
output=None,
path_filter=None,
filter_recursive_only=False,
root=None,
shell_globbing=False
):
if input:
self.input = input
else:
self.input = sys.stdin
if output:
self.output = output
else:
self.output = sys.stdout
self.path_filter = path_filter
# Make sure there are no trailing slashes.
if root:
self.root = root.rstrip('/')
self.root_slash = root + '/'
self.filter_recursive_only = filter_recursive_only
self.shell_globbing = shell_globbing
def _get_relative_root(self, path):
if self.root:
if path.startswith('/'):
path = path.lstrip('/')
path = os.path.join(self.root, path)
path = os.path.realpath(path)
if self.root:
if not path.startswith(self.root_slash) and path != self.root:
# A symlink or .. broke us out of the root.
# Do not allow this.
raise RootEscapeError
return path
def _remove_root(self, path):
if self.root:
if path.startswith(self.root):
path = path[len(self.root):]
return path
def _filter(self, path):
if self.path_filter:
return not self.path_filter(path)
else:
return False
def receive(self, preserve, recursive, target_should_be_dir, target):
"""Receive file(s).
This is the -t flag.
:Parameters:
- `preserve`: If true, attempts to preserve modification time,
access time, and mode of the original file.
- `recursive`: If true, will recursively send files.
- `target_should_be_dir`: If true, the target should be a directory.
- `target`: The target for the file(s).
"""
if preserve:
# Clear the mask so that the mode can be properly preserved.
os.umask(0)
if '\n' in target:
self.soft_error('target (%r) contains a newline' % (target,))
return
try:
relative_target = self._get_relative_root(target)
except RootEscapeError:
self.soft_error('%s: target not available.' % (target,))
return
if self._filter(relative_target):
self.soft_error('%s: target not available.' % (target,))
return
target_is_dir = os.path.isdir(relative_target)
if target_should_be_dir and not target_is_dir:
self.soft_error('Expected target %s to be a directory.' % (target,))
return
self._respond_success()
got_timestamp = False
mtime_sec = mtime_usec = atime_sec = atime_usec = 0
while 1:
result = []
if not self._read_line(result):
return
result = ''.join(result)
self.debug(DEBUG_FULL, 'Executing command: %r', result)
try:
code = result[0]
except IndexError:
self.hard_error('Expected command character.')
result = result[1:]
if code == '\01':
self._report_error(result[1:])
self.had_errors = True
elif code == '\02':
self._report_error(result[1:])
sys.exit(1)
elif code == 'E':
self._respond_success()
return
elif code == 'T':
parts = result.split()
if len(parts) != 4:
self.hard_error('Timestamp command format error (%r)' % (result,))
try:
mtime_sec = int(parts[0])
mtime_usec = int(parts[1])
atime_sec = int(parts[2])
atime_usec = int(parts[3])
except ValueError:
self.hard_error('Invalid timestamp value (%r)' % (result,))
else:
got_timestamp = True
self._respond_success()
elif code == 'C' or code == 'D':
if len(result) < 8:
# Minimum for 4 bytes (mode), space, length (min 1 byte),
# space, name (min 1 byte).
self.hard_error('Command length too short (%r)' % (result,))
try:
mode = int(result[:4], 8)
except ValueError:
self.hard_error('Invalid file mode (%r)' % (result,))
if result[4] != ' ':
self.hard_error('Command not properly delimited (%r)' % (result,))
try:
end = result.index(' ', 5)
except ValueError:
self.hard_error('Command not properly delimited (%r)' % (result,))
try:
size = int(result[5:end])
except ValueError:
self.hard_error('Invalid size (%r)' % (result,))
filename = result[end+1:]
if not filename:
self.hard_error('Filename not specified (%r)' % (result,))
if '/' in filename or filename == '..':
self.hard_error('Invalid filename (%r)' % (result,))
if target_is_dir:
# Store the file in the target directory.
relative_pathname = os.path.join(target, filename)
else:
# Store the file as the target.
relative_pathname = target
try:
absolute_pathname = self._get_relative_root(relative_pathname)
except RootEscapeError:
self.soft_error('%s: target not available.' % (relative_pathname,))
return
if self._filter(absolute_pathname):
self.soft_error('%s: target not available.' % (relative_pathname,))
return
self.debug(DEBUG_EXTRA, 'Receiving file %r mode %o size %i in dir %r' % (filename, mode, size, target))
if code == 'D':
# Creating a directory.
if not recursive:
self.hard_error('received directory without -r')
if os.path.exists(absolute_pathname):
if not os.path.isdir(absolute_pathname):
self.soft_error('Target (%r) exists, but is not a directory.' % (relative_pathname,))
continue
if preserve:
try:
os.chmod(absolute_pathname, mode)
except OSError, e:
self.debug(DEBUG_NORMAL, 'Failed to chmod %r to %o: %s', relative_pathname, mode, e.strerror)
# Continue, this is not critical.
else:
try:
os.mkdir(absolute_pathname, mode)
except OSError, e:
self.soft_error('Unable to make directory (%r): %s' % (relative_pathname, e.strerror))
continue
self.receive(preserve, recursive, target_should_be_dir, relative_pathname)
if got_timestamp:
got_timestamp = False
timestamp = (mtime_sec, atime_sec)
try:
os.utime(absolute_pathname, timestamp)
except OSError, e:
self.soft_error('Failed to set timestamp (%r) on %r: %s' % (timestamp, relative_pathname, e.strerror))
continue
else:
# code == 'C'
# Creating a file.
try:
fd = os.open(absolute_pathname, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, mode)
except OSError, e:
self.soft_error('Failed to create %r: %s' % (relative_pathname, e.strerror))
continue
try:
self.debug(DEBUG_FULL, 'Send response before start.')
self._respond_success()
bytes_left = size
error = None
while bytes_left > 0:
block = self.input.read(min(bytes_left, self.blocksize))
if not block:
self.hard_error('End of file, but expected more data while ready %r.' % (relative_pathname,))
bytes_left -= len(block)
if not error:
try:
os.write(fd, block)
except OSError, e:
error = e
finally:
os.close(fd)
self.read_response()
if got_timestamp:
got_timestamp = False
timestamp = (atime_sec, mtime_sec)
self.debug(DEBUG_EXTRA, 'Setting timestamp of %r to %r.', relative_pathname, timestamp)
try:
os.utime(absolute_pathname, timestamp)
except OSError, e:
self.soft_error('Failed to set timestamp (%r) on %r: %s' % (timestamp, relative_pathname, e.strerror))
continue
if error:
self.soft_error('Error while writing %r: %s' % (relative_pathname, error.strerror))
else:
self._respond_success()
else:
# Unknown command.
self.hard_error('Invalid command: %r' % (result,))
def send(self, preserve, recursive, pathnames):
"""Send file(s).
This is the -f flag. Make sure you call read_response before calling
this the first time.
:Parameters:
- `preserve`: If true, attempts to preserve modification time,
access time, and mode of the original file.
- `recursive`: If true, will recursively send files.
- `pathnames`: List of pathnames to send.
"""
for relative_pathname in pathnames:
if '\n' in relative_pathname:
self.soft_error('skipping, filename (%r) contains a newline' % (relative_pathname,))
continue
# Remove any trailing slashes.
relative_pathname = relative_pathname.rstrip('/')
try:
absolute_pathname = self._get_relative_root(relative_pathname)
except RootEscapeError:
self.soft_error('%s: Invalid pathname.' % (relative_pathname,))
return
# Potentially do shell expansion.
if self.shell_globbing:
more_absolute_pathnames = glob.glob(absolute_pathname)
if not more_absolute_pathnames:
self.soft_error('%s: No such file or directory.' % (relative_pathname,))
continue
else:
more_absolute_pathnames = [absolute_pathname]
for more_absolute_pathname in more_absolute_pathnames:
more_relative_pathname = self._remove_root(more_absolute_pathname)
# If the original pathname provided was relative, leave the
# globbed versions as relative.
if not relative_pathname.startswith('/'):
more_relative_pathname = more_relative_pathname.lstrip('/')
self._send(preserve,
recursive,
more_relative_pathname,
more_absolute_pathname
)
def _send(self, preserve, recursive, relative_pathname, absolute_pathname):
if not self.filter_recursive_only and self._filter(absolute_pathname):
self.soft_error('%s: No such file or directory' % (relative_pathname,))
return
try:
st = os.stat(absolute_pathname)
except OSError, e:
self.soft_error('%s: %s' % (relative_pathname, e.strerror))
return
if stat.S_ISDIR(st.st_mode):
if self.filter_recursive_only and self._filter(absolute_pathname):
self.soft_error('%s: No such file or directory' % (relative_pathname,))
return
if recursive:
self._send_recursive(preserve, relative_pathname, absolute_pathname, st)
else:
self.soft_error('%s: skipping, is a directory and -r not specified' % (relative_pathname,))
elif stat.S_ISREG(st.st_mode):
self._send_file(preserve, relative_pathname, absolute_pathname, st)
else:
self.soft_error('%s: skipping, not a regular file' % (relative_pathname,))
def _send_file(self, preserve, relative_pathname, absolute_pathname, st):
# pathname should already have been filtered.
if preserve:
if not self._send_preserve_timestamp(st):
return
base = os.path.basename(relative_pathname)
self.debug(DEBUG_NORMAL, 'Sending file %r', relative_pathname)
self.output.write('C%04o %i %s\n' % (stat.S_IMODE(st.st_mode),
st.st_size,
base
)
)
self.output.flush()
if not self.read_response():
return
try:
fd = os.open(absolute_pathname, os.O_RDONLY)
except OSError, e:
self.soft_error('%s: %s' % (relative_pathname, e.strerror))
return
try:
bytes_left = st.st_size
error = None
while bytes_left > 0:
if bytes_left < self.blocksize:
to_read = bytes_left
else:
to_read = self.blocksize
block = os.read(fd, to_read)
if not block:
error = '%s: File shrunk while reading.' % (relative_pathname,)
# Keep writing to stay in sync.
dummy_data = '\0' * self.blocksize
while bytes_left > 0:
if bytes_left < len(dummy_data):
dummy_data = dummy_data[:bytes_left]
self.output.write(dummy_data)
bytes_left -= len(dummy_data)
break
self.output.write(block)
bytes_left -= len(block)
finally:
os.close(fd)
self.output.flush()
if error:
self.soft_error(error)
else:
self.debug(DEBUG_FULL, 'File send complete, sending success, waiting for response.')
self._respond_success()
self.read_response()
def _send_recursive(self, preserve, relative_pathname, absolute_pathname, st):
self.debug(DEBUG_EXTRA, 'Send recursive %r', relative_pathname)
try:
files = os.listdir(absolute_pathname)
except OSError, e:
self.soft_error('%s: %s' % (relative_pathname, e.strerror))
return
if preserve:
if not self._send_preserve_timestamp(st):
return
base = os.path.basename(relative_pathname)
self.debug(DEBUG_NORMAL, 'Sending directory %r', relative_pathname)
self.output.write('D%04o %i %s\n' % (stat.S_IMODE(st.st_mode),
0,
base
)
)
self.output.flush()
if not self.read_response():
return
for filename in files:
recursive_relative_pathname = os.path.join(relative_pathname, filename)
try:
recursive_absolute_pathname = self._get_relative_root(recursive_relative_pathname)
except RootEscapeError:
continue
if self._filter(recursive_absolute_pathname):
continue
self.send(preserve, True, [recursive_relative_pathname])
self.debug(DEBUG_FULL, 'Sending end of transmission.')
self.output.write('E\n')
self.output.flush()
self.read_response()
self.debug(DEBUG_FULL, 'End of transmission response read.')
def _send_preserve_timestamp(self, st):
self.debug(DEBUG_EXTRA, 'Sending preserve timestamp %i %i', st.st_mtime, st.st_atime)
self.output.write('T%i 0 %i 0\n' % (st.st_mtime, st.st_atime))
self.output.flush()
if self.read_response():
return True
else:
return False
def _respond_success(self):
self.output.write('\0')
self.output.flush()
def soft_error(self, error):
"""Sends a soft-error response.
:Parameters:
- `error`: The error string to send. This should not have any
newline characters.
"""
self.debug(DEBUG_NORMAL, 'Soft error: %s' % (error,))
self.output.write('\01scp: %s\n' % (error,))
self.output.flush()
self._report_error(error)
self.had_errors = True
def hard_error(self, error):
"""Sends a hard-error response.
This function calls sys.exit.
:Parameters:
- `error`: The error string to send. This should not have any
newline characters.
"""
self.debug(DEBUG_NORMAL, 'Hard error: %s' % (error,))
self.output.write('\02scp: %s\n' % (error,))
self.output.flush()
self._report_error(error)
sys.exit(1)
def read_response(self):
"""Read a response.
This function calls sys.exit on fatal errors.
:Return:
Returns True on a successful response, False otherwise.
"""
code = self.input.read(1)
self.debug(DEBUG_FULL, 'Got response %r' % (code,))
if code == '\0':
return True
else:
if code == '\1' or code == '\2':
result = []
else:
result = [code]
self._read_line(result)
result = ''.join(result)
self._report_error(result)
self.had_errors = True
if code == '\1':
return False
else:
sys.exit(1)
def _read_line(self, result):
for unused in xrange(self.max_line_size):
char = self.input.read(1)
if not char:
if not result:
return False
else:
self.hard_error('End of line, expected \\n')
if char == '\n':
break
result.append(char)
else:
# Never saw a \n.
self.hard_error('Command line too long (%i)' % (self.max_line_size,))
return True
def _report_error(self, message):
"""Report an error message.
By default, this does nothing (since the base code is intended for use
on the server side). The client side should send the message to stderr
or its equivalent.
:Parameters:
- `message`: The message to print (should not have a newline).
"""
pass
def debug(self, level, format, *args):
"""Send a debug message.
:Parameters:
- `level`: The level of the message. May be DEBUG_NORMAL,
DEBUG_EXTRA, or DEBUG_FULL.
- `format`: The string to write. Will be applied with the Python
format operator with the rest of the arguments.
"""
if level <= self.verbosity:
msg = format % args
print >>sys.stderr, '%s %i:%s' % (time.ctime(), level, msg)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# NOTE: THIS DOES NOT WORK.
# There is a problem with reading from stdin via kqueue.
# I'm not sure (yet) what exactly is wrong.
import ssh.transport.client
import ssh.connection.connect
import ssh.l4_transport.coro_socket_transport
import ssh.auth.userauth
import ssh.connection.interactive_session
import getopt
import sys
import termios
import fcntl
import os
import ssh.util.debug
import inet_utils
import socket
import coro
def usage():
print 'test_coro_client [-l login_name] hostname | user@hostname'
oldterm = None
oldflags = None
def set_stdin_unbuffered():
global oldterm, oldflags
oldterm = termios.tcgetattr(0)
newattr = termios.tcgetattr(0)
newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
termios.tcsetattr(0, termios.TCSANOW, newattr)
oldflags = fcntl.fcntl(0, fcntl.F_GETFL)
fcntl.fcntl(0, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
def input_thread(channel):
stdin = coro.fd_sock(0)
while 1:
data = stdin.recv(100)
channel.send(data)
def transport_thread(channel):
while not channel.eof and not channel.closed:
data = channel.read(1024)
if data:
os.write(1, data)
def doit(ip):
debug = ssh.util.debug.Debug()
#debug.level = ssh.util.debug.DEBUG_3
client = ssh.transport.client.SSH_Client_Transport(debug=debug)
transport = ssh.l4_transport.coro_socket_transport.coro_socket_transport(ip)
client.connect(transport)
auth_method = ssh.auth.userauth.Userauth(client)
service = ssh.connection.connect.Connection_Service(client)
client.authenticate(auth_method, service.name)
channel = ssh.connection.interactive_session.Interactive_Session_Client(service)
channel.open()
channel.open_pty()
channel.open_shell()
set_stdin_unbuffered()
coro.spawn(input_thread, channel)
coro.spawn(transport_thread, channel)
def main():
login_username = None
ip = None
try:
optlist, args = getopt.getopt(sys.argv[1:], 'l:')
except getopt.GetoptError, why:
print str(why)
usage()
sys.exit(1)
for option, value in optlist:
if option=='l':
login_username = value
if len(args) != 1:
usage()
sys.exit(1)
ip = args[0]
if '@' in ip:
login_username, ip = ip.split('@', 1)
if not inet_utils.is_ip(ip):
ip = socket.gethostbyname(ip)
coro.spawn(doit, ip)
try:
coro.event_loop()
finally:
if oldterm:
termios.tcsetattr(0, termios.TCSAFLUSH, oldterm)
fcntl.fcntl(0, fcntl.F_SETFL, oldflags)
if __name__=='__main__':
main()
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# test_ssh_transport
#
# Test routines for ssh_transport.
#
# Broken into a separate file because it's quite large.
#
import ssh.l4_transport
import ssh.util.packet
import ssh.transport
import ssh.key_exchange
import ssh.transport.transport
import ssh.transport.client
from ssh.keys.public_private_key import SSH_Public_Private_Key
from ssh.transport.transport import One_Way_SSH_Transport
from ssh.transport.constants import *
import unittest
class ssh_transport_test_case(unittest.TestCase):
pass
class Null_Transport(ssh.l4_transport.Transport):
def connect(self):
return
def read(self, bytes):
return ''
def write(self, data):
return len(data)
def read_line(self):
return ''
def close(self):
return
def get_host_id(self):
return None
class kexinit_test_case(ssh_transport_test_case):
def runTest(self):
# Simple Test
# Can't instantiate SSH_Transport directly.
a = ssh.transport.client.SSH_Client_Transport()
a.transport = Null_Transport()
# Prepare kexinit packet.
a._send_kexinit()
# Create a fake packet.
fake_kexinit_packet = ssh.util.packet.pack_payload(
ssh.util.packet.PAYLOAD_MSG_KEXINIT, (
SSH_MSG_KEXINIT,
'A'*16, # cookie
['diffie-hellman-group1-sha1'],
['ssh-dss'],
['3des-cbc'],
['3des-cbc'],
['hmac-sha1'],
['hmac-sha1'],
['none'],
['none'],
[],
[],
0, # first_kex_packet_follows
0 # reserved
)
)
a.msg_kexinit(fake_kexinit_packet)
# Set preferred algorithms.
a.send_newkeys()
# Make sure algorithms were set properly.
self.assertEqual(a.key_exchange.name, 'diffie-hellman-group1-sha1')
self.assertEqual(a.server_key.name, 'ssh-dss')
self.assertEqual(a.c2s.cipher.name, '3des-cbc')
self.assertEqual(a.s2c.cipher.name, '3des-cbc')
self.assertEqual(a.c2s.mac.name, 'hmac-sha1')
self.assertEqual(a.s2c.mac.name, 'hmac-sha1')
self.assertEqual(a.c2s.compression.name, 'none')
self.assertEqual(a.s2c.compression.name, 'none')
# Complex Test 1
a = ssh_client.SSH_Client_Transport()
a.transport = Null_Transport()
# Prepare kexinit packet.
a._send_kexinit()
# Create a fake packet.
fake_kexinit_packet = ssh.util.packet.pack_payload(
ssh.util.packet.PAYLOAD_MSG_KEXINIT, (
SSH_MSG_KEXINIT,
'A'*16, # cookie
['foobar', 'diffie-hellman-group1-sha1'],
['fake-server-key-type', 'ssh-dss'],
['encrypt-this', '3des-cbc'],
['xor', '3des-cbc'],
['none', 'hmac-sha1'],
['mac-daddy', 'hmac-sha1'],
['zzz', 'none'],
['aaa', 'none'],
['pig-latin'],
[],
0, # first_kex_packet_follows
0 # reserved
)
)
a.msg_kexinit(fake_kexinit_packet)
# Set preferred algorithms.
a.send_newkeys()
# Make sure algorithms were set properly.
self.assertEqual(a.key_exchange.name, 'diffie-hellman-group1-sha1')
self.assertEqual(a.server_key.name, 'ssh-dss')
self.assertEqual(a.c2s.cipher.name, '3des-cbc')
self.assertEqual(a.s2c.cipher.name, '3des-cbc')
self.assertEqual(a.c2s.mac.name, 'hmac-sha1')
self.assertEqual(a.s2c.mac.name, 'hmac-sha1')
self.assertEqual(a.c2s.compression.name, 'none')
self.assertEqual(a.s2c.compression.name, 'none')
# Complex Test 2
class funky_one_way(One_Way_SSH_Transport):
def __init__(self, transport):
self.supported_macs.reverse()
One_Way_SSH_Transport.__init__(self, transport)
a = ssh_client.SSH_Client_Transport(client_transport = funky_one_way())
a.transport = Null_Transport()
# Prepare kexinit packet.
a._send_kexinit()
# Create a fake packet.
fake_kexinit_packet = ssh.util.packet.pack_payload(
ssh.util.packet.PAYLOAD_MSG_KEXINIT, (
SSH_MSG_KEXINIT,
'A'*16, # cookie
['foobar', 'diffie-hellman-group1-sha1'],
['fake-server-key-type', 'ssh-dss'],
['encrypt-this', '3des-cbc'],
['xor', '3des-cbc'],
['hmac-sha1', 'none'],
['mac-daddy', 'hmac-sha1'],
['zzz', 'none'],
['aaa', 'none'],
['pig-latin'],
[],
0, # first_kex_packet_follows
0 # reserved
)
)
a.msg_kexinit(fake_kexinit_packet)
# Set preferred algorithms.
a.send_newkeys()
# Make sure algorithms were set properly.
self.assertEqual(a.key_exchange.name, 'diffie-hellman-group1-sha1')
self.assertEqual(a.server_key.name, 'ssh-dss')
self.assertEqual(a.c2s.cipher.name, '3des-cbc')
self.assertEqual(a.s2c.cipher.name, '3des-cbc')
self.assertEqual(a.c2s.mac.name, 'none')
self.assertEqual(a.s2c.mac.name, 'hmac-sha1')
self.assertEqual(a.c2s.compression.name, 'none')
self.assertEqual(a.s2c.compression.name, 'none')
# Mismatch Test1
class bogus_ssh_server_key(SSH_Public_Private_Key):
supports_signature = 0
supports_encryption = 0
name = 'bogus'
class funky_one_way2(One_Way_SSH_Transport):
supported_server_keys = [bogus_ssh_server_key]
a = ssh_client.SSH_Client_Transport(client_transport = funky_one_way2())
a.transport = Null_Transport()
# Prepare kexinit packet.
a._send_kexinit()
# Create a fake packet.
fake_kexinit_packet = ssh.util.packet.pack_payload(
ssh.util.packet.PAYLOAD_MSG_KEXINIT, (
SSH_MSG_KEXINIT,
'A'*16, # cookie
['foobar', 'diffie-hellman-group1-sha1'],
['fake-server-key-type', 'ssh-dss'],
['encrypt-this', '3des-cbc'],
['xor', '3des-cbc'],
['hmac-sha1', 'none'],
['mac-daddy', 'hmac-sha1'],
['zzz', 'none'],
['aaa', 'none'],
['pig-latin'],
[],
0, # first_kex_packet_follows
0 # reserved
)
)
self.assertRaises(ssh.transport.SSH_Protocol_Error, a.msg_kexinit, fake_kexinit_packet)
# Mismatch Test2
self.test_matchup_kex_and_key(['one', 'two'],
['two', 'one'],
['a', 'b'],
['b', 'c'],
{'one': {'wants_enc': 1,
'wants_sig': 1},
'two': {'wants_enc': 1,
'wants_sig': 1},
},
{'a': {'enc': 0,
'sig': 1},
'b': {'enc': 1,
'sig': 0},
'c': {'enc': 1,
'sig': 1}
},
1, # Expects exception.
None, None)
def test_matchup_kex_and_key(self, c2s_kex_supported, s2c_kex_supported,
c2s_key_supported, s2c_key_supported,
kex_features, key_features,
expected_exception,
expected_kex,
expected_key):
import new
c2s_kex = []
for kex in c2s_kex_supported:
f = new.classobj(kex, (ssh.key_exchange.SSH_Key_Exchange,), {})
f.name = kex
f.wants_signature_host_key = kex_features[kex]['wants_sig']
f.wants_encryption_host_key = kex_features[kex]['wants_enc']
c2s_kex.append(f)
s2c_kex = []
for kex in s2c_kex_supported:
f = new.classobj(kex, (ssh.key_exchange.SSH_Key_Exchange,), {})
f.name = kex
f.wants_signature_host_key = kex_features[kex]['wants_sig']
f.wants_encryption_host_key = kex_features[kex]['wants_enc']
s2c_kex.append(f)
c2s_key = []
for key in c2s_key_supported:
k = new.classobj(key, (SSH_Public_Private_Key,), {})
k.name = key
k.supports_signature = key_features[key]['sig']
k.supports_encryption = key_features[key]['enc']
c2s_key.append(k)
s2c_key = []
for key in s2c_key_supported:
k = new.classobj(key, (SSH_Public_Private_Key,), {})
k.name = key
k.supports_signature = key_features[key]['sig']
k.supports_encryption = key_features[key]['enc']
s2c_key.append(k)
class client_transport(One_Way_SSH_Transport):
supported_key_exchanges = c2s_kex
supported_server_keys = c2s_key
class server_transport(One_Way_SSH_Transport):
supported_key_exchanges = s2c_key
supported_server_keys = s2c_key
import ssh_client
a = ssh_client.SSH_Client_Transport(client_transport=client_transport(), server_transport=server_transport())
a.transport = Null_Transport()
# Prepare kexinit packet.
a._send_kexinit()
# Create a fake packet.
fake_kexinit_packet = ssh.util.packet.pack_payload(
ssh.util.packet.PAYLOAD_MSG_KEXINIT, (
SSH_MSG_KEXINIT,
'A'*16, # cookie
s2c_kex_supported,
s2c_key_supported,
['3des-cbc'],
['3des-cbc'],
['hmac-sha1'],
['hmac-sha1'],
['none'],
['none'],
[],
[],
0, # first_kex_packet_follows
0 # reserved
)
)
if expected_exception:
self.assertRaises(ssh.transport.SSH_Protocol_Error, a.msg_kexinit, fake_kexinit_packet)
else:
a.msg_kexinit(fake_kexinit_packet)
# Set preferred algorithms.
a.send_newkeys()
# Make sure algorithms were set properly.
self.assertEqual(a.key_exchange.name, expected_kex)
self.assertEqual(a.server_key.name, expected_key)
def suite():
suite = unittest.TestSuite()
suite.addTest(kexinit_test_case())
return suite
if __name__ == '__main__':
unittest.main(module='test_ssh_transport', defaultTest='suite')
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.transport
#
# This is the core SSH transport. It handles the low-level
# transmission of data.
#
__version__ = '$Revision: #1 $'
class SSH_Protocol_Error(Exception):
pass
class Stop_Message_Processing(Exception):
pass
class SSH_Service:
name = ''
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.transport.client
#
# This implements the client functionality of the SSH transport layer.
#
__version__ = '$Revision: #1 $'
import coro
import ssh.util.debug
import ssh.util.packet
import ssh.transport.transport
import ssh.keys.key_storage
from ssh.transport.constants import *
class SSH_Client_Transport(ssh.transport.transport.SSH_Transport):
def __init__(self, client_transport=None, server_transport=None, debug=None):
ssh.transport.transport.SSH_Transport.__init__(self, client_transport, server_transport, debug)
self.self2remote = self.c2s
self.remote2self = self.s2c
def connect(self, transport):
"""connect(self, transport) -> None
Connect to the remote host.
"""
try:
self._connect(transport)
except:
# Any exception is fatal.
self.disconnect()
raise
def _connect(self, transport):
self.transport = transport
transport.connect()
# Send identification string.
if self.c2s.comments:
comments = ' ' + self.c2s.comments
else:
comments = ''
self.c2s.version_string = 'SSH-' + self.c2s.protocol_version + '-' + self.c2s.software_version + comments
transport.write(self.c2s.version_string + '\r\n')
# Receive server's identification string.
while 1:
# We might receive lines before we get the version. Just ignore
# them.
# Note that the SSH spec says that clients MUST be able to receive
# a version string that does not end in CRLF. It appears that not
# even OpenSSH does this. Plus, the spec does not indicate how to
# determine the length of the identification string. We're not
# going to bother supporting that here. It's for backwards
# compatibility.
line = transport.read_line()
if line.startswith('SSH-'):
# Got the identification string.
self.s2c.version_string = line
# See if there are any optional comments.
i = line.find(' ')
if i!=-1:
self.s2c.comments = line[i+1:]
line = line[:i]
# Break up the identification string into its parts.
parts = line.split('-')
if len(parts) != 3:
self.send_disconnect(ssh.transport.transport.SSH_DISCONNECT_PROTOCOL_ERROR, 'server identification invalid: %r' % line)
self.s2c.protocol_version = parts[1]
self.s2c.software_version = parts[2]
if self.s2c.protocol_version not in ('1.99', '2.0'):
self.send_disconnect(ssh.transport.transport.SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, 'protocol version not supported: %r' % self.s2c.protocol_version)
break
self.send_kexinit()
if self.self2remote.proactive_kex:
# Go ahead and send our kex packet with our preferred algorithm.
# This will assume the server side supports the algorithm.
self.c2s.set_preferred('key_exchange')
self.c2s.set_preferred('server_key')
self.set_key_exchange()
packet = self.c2s.key_exchange.get_initial_client_kex_packet()
self.send_packet(packet)
# Start the receive thread.
# This depends on coro behavior that the thread won't start until we go to sleep
# (which will happen in _process_kexinit).
self.start_receive_thread()
# Receive server kexinit
self._process_kexinit()
self.debug.write(ssh.util.debug.DEBUG_3, 'key exchange: got kexinit')
if not self.self2remote.proactive_kex:
packet = self.key_exchange.get_initial_client_kex_packet()
if packet:
# It is possible for a key exchange algorithm to not have
# an initial packet to send on the client side.
self.send_packet(packet)
# Let the key exchange finish.
# XXX: Need to lock down to prevent any non-key exchange packets from being transferred.
# Key exchange finished and SSH_MSG_NEWKEYS sent, wait for the remote side to send NEWKEYS.
message_type, packet = self.receive_message((SSH_MSG_NEWKEYS,))
self.msg_newkeys(packet)
# XXX: Unlock key exchange lockdown for c2s.
def authenticate(self, authentication_method, service_name):
"""authenticate(self, authentication_method, service) -> None
Authenticate with the remote side.
<authentication_method>:
<service_name>: The name of the service that you want to use after
authenticating. Typically 'ssh-connection'.
"""
# Ask the remote side if it is OK to use this authentication service.
self.debug.write(ssh.util.debug.DEBUG_3, 'authenticate: sending service request (%s)', (authentication_method.name,))
service_request_packet = ssh.util.packet.pack_payload(ssh.util.packet.PAYLOAD_MSG_SERVICE_REQUEST,
(ssh.transport.transport.SSH_MSG_SERVICE_REQUEST,
authentication_method.name))
self.send_packet(service_request_packet)
# Server will disconnect if it doesn't like our service request.
self.debug.write(ssh.util.debug.DEBUG_3, 'authenticate: waiting for SERVICE_ACCEPT')
message_type, packet = self.receive_message((ssh.transport.transport.SSH_MSG_SERVICE_ACCEPT,))
msg, accepted_service_name = ssh.util.packet.unpack_payload(ssh.util.packet.PAYLOAD_MSG_SERVICE_ACCEPT, packet)
self.debug.write(ssh.util.debug.DEBUG_3, 'authenticate: got SERVICE_ACCEPT')
if accepted_service_name != authentication_method.name:
self.send_disconnect(ssh.transport.transport.SSH_DISCONNECT_PROTOCOL_ERROR, 'accepted service does not match requested service "%s"!="%s"' % (authentication_method.name, accepted_service_name))
# This authetnication service is OK, try to authenticate.
authentication_method.authenticate(service_name)
def request_service(self, service_instance):
"""request_service(self, service_instance) -> None
Requests to run this service over the transport.
If the service is not available, then a disconnect will be sent.
<service_instance> is a SSH_Service class instance.
"""
#SSH_MSG_SERVICE_REQUEST
pass
def msg_service_request_response(self, packet):
pass
def verify_public_host_key(self, public_host_key, username=None):
"""verify_public_host_key(self, public_host_key, username=None) -> None
Verifies that the given public host key is the correct public key for
the current remote host.
<public_host_key>: A SSH_Public_Private_Key instance.
Raises Invalid_Server_Public_Host_Key exception if it does not match.
"""
host_id = self.transport.get_host_id()
for storage in self.supported_key_storages:
if storage.verify(host_id, self.c2s.supported_server_keys, public_host_key, username):
return
raise ssh.keys.key_storage.Invalid_Server_Public_Host_Key(host_id, public_host_key)
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.transport.constants
#
# Constants used in the transport modules.
#
SSH_MSG_DISCONNECT = 1
SSH_MSG_IGNORE = 2
SSH_MSG_UNIMPLEMENTED = 3
SSH_MSG_DEBUG = 4
SSH_MSG_SERVICE_REQUEST = 5
SSH_MSG_SERVICE_ACCEPT = 6
SSH_MSG_KEXINIT = 20
SSH_MSG_NEWKEYS = 21
SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1
SSH_DISCONNECT_PROTOCOL_ERROR = 2
SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3
SSH_DISCONNECT_RESERVED = 4
SSH_DISCONNECT_MAC_ERROR = 5
SSH_DISCONNECT_COMPRESSION_ERROR = 6
SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7
SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8
SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9
SSH_DISCONNECT_CONNECTION_LOST = 10
SSH_DISCONNECT_BY_APPLICATION = 11
SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12
SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13
SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14
SSH_DISCONNECT_ILLEGAL_USER_NAME = 15
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_transport
#
# This module implements the SSH Transport layer.
# This is the lowest-level layer of the SSH Protocol. It does NOT implement
# authentication or anything else.
#
# This implements the SSH2 protocol ONLY.
#
__version__ = '$Revision: #1 $'
import coro
import struct
import sys
import ssh.util.debug
import ssh.util.packet
import ssh.util.random
import ssh.util
import ssh.transport
from ssh.transport.constants import *
from ssh.key_exchange.diffie_hellman import Diffie_Hellman_Group1_SHA1
from ssh.keys.dss import SSH_DSS
from ssh.keys.rsa import SSH_RSA
from ssh.cipher.des3_cbc import Triple_DES_CBC
from ssh.cipher.blowfish_cbc import Blowfish_CBC
from ssh.mac.hmac_sha1 import HMAC_SHA1
from ssh.mac.hmac_md5 import HMAC_MD5
from ssh.mac.none import MAC_None
from ssh.cipher.none import Cipher_None
from ssh.compression.none import Compression_None
from ssh.keys.openssh_key_storage import OpenSSH_Key_Storage
class SSH_Transport:
# The low-level OS transport abstraction.
transport = None
# These are references to the in-use entry from one of the
# supported_xxx lists.
# These two values are not set until after we have received the remote
# side's kexinit packet. (Thus, you can not rely on them when making
# a proactive guess before the kexinit packet has arrived.)
key_exchange = None
server_key = None
# Set this to true if we failed to guess the correct kex algorithm.
ignore_first_packet = False
# Normally One_Way_SSH_Transport instances.
# You can subclass this class to use a different
# transport that supports different algorithms.
c2s = None # client to server ssh transport
s2c = None # server to client ssh transport
# These are references to the appropriate c2s or s2c object.
self2remote = None
remote2self = None
# List of key_storage instances.
supported_key_storages = None
# Boolean whether or not we are the server.
is_server = False
# Thread object for reading from the socket.
_receive_thread = None
# Flag to indicate whether or not this connection is closed.
closed = True
def __init__(self, client_transport=None, server_transport=None, debug=None):
self.tmc = Thread_Message_Callbacks()
self.send_mutex = coro.mutex()
# This is the registry of modules that want to receive certain messages.
# The key is the module name, the value is a dictionary of message number
# to callback function. The function takes 1 parameter (the packet).
self.message_callback_registry = {}
# This is a mapping of SSH message numbers to the function to call when
# that message is received. It is an optimized version computed from
# message_callback_registry.
self.message_callbacks = {}
if debug is None:
self.debug = ssh.util.debug.Debug()
else:
self.debug = debug
if client_transport is None:
self.c2s = One_Way_SSH_Transport(self)
else:
self.c2s = client_transport
if server_transport is None:
self.s2c = One_Way_SSH_Transport(self)
else:
self.s2c = server_transport
self.supported_key_storages = [OpenSSH_Key_Storage()]
self.register_callbacks('__base__',
{SSH_MSG_IGNORE: self.msg_ignore,
SSH_MSG_DEBUG: self.msg_debug,
SSH_MSG_DISCONNECT:self.msg_disconnect,
#SSH_MSG_KEXINIT:self.msg_kexinit,
#SSH_MSG_NEWKEYS:self.msg_newkeys,
}
)
def unregister_callbacks(self, module_name):
"""unregister_callbacks(self, module_name) -> None
Remove the given module from the registry.
"""
try:
del self.message_callback_registry[module_name]
except KeyError:
pass
self._recompile_callback_registry()
def register_callbacks(self, module_name, callback_dict):
"""register_callbacks(self, module_name, callback_dict) -> None
Registers the given module_name (a string) the given callbacks.
<callback_dict> is a dictionary of message number to callback function.
If there were callbacks previously registered under the same name,
this will clear the previous values.
If more than one module is listening for the same message numbers,
it is not deterministic which one will receive the message.
In other words, don't do that!
Also, beware that some message numbers can be the same even if they
are referenced with different names. An example is
SSH_MSG_KEX_DH_GEX_GROUP and SSH_MSG_KEXDH_REPLY both are the
number 31.
"""
self.debug.write(ssh.util.debug.DEBUG_3, 'register_callbacks(module_name=%s, callback_dict=...)', (module_name,))
self.message_callback_registry[module_name] = callback_dict
self._recompile_callback_registry()
def _recompile_callback_registry(self):
# Recompile message_callbacks
self.message_callbacks = {}
for dictn in self.message_callback_registry.values():
self.message_callbacks.update(dictn)
def disconnect(self):
"""disconnect(self) -> None
Closes the connection.
"""
if not self.closed:
self.stop_receive_thread()
self.closed = True
# Make an alias.
close = disconnect
def send_disconnect(self, reason_code, description):
"""send_disconnect(self, reason_code, description) -> None
"""
self.debug.write(ssh.util.debug.DEBUG_3, 'send_disconnect(reason_code=%r, description=%r)', (reason_code, description))
# Langauge tag currently set to the empty string.
language_tag = ''
self.send_packet(
ssh.util.packet.pack_payload(ssh.util.packet.PAYLOAD_MSG_DISCONNECT,
(SSH_MSG_DISCONNECT,
reason_code,
description,
language_tag)
)
)
self.disconnect()
raise ssh.transport.SSH_Protocol_Error, (reason_code, description)
def send_packet(self, data):
"""send_packet(self, data) -> None
Sends the given packet data.
<data>: A string.
"""
self.send_mutex.lock()
try:
try:
self._send_packet(data)
except:
# Any error is fatal.
self.disconnect()
raise
finally:
self.send_mutex.unlock()
def _send_packet(self, data):
# Packet is:
# uint32 packet_length
# byte padding_length
# byte[n1] payload
# byte[n2] random padding
# byte[m] MAC
self.debug.write(ssh.util.debug.DEBUG_3, 'send_packet( len(data)=%i )', (len(data),))
data = self.self2remote.compression.compress(data)
# packet_len + padding_length + payload + random_padding
# must be multiple of cipher block size.
block_size = max(8, self.self2remote.cipher.block_size)
padding_length = block_size - ((5 + len(data)) % block_size)
if padding_length < 4:
padding_length += block_size
# Total packet size must also be at least 16 bytes.
base_size = 5 + len(data) + padding_length
minimum_size = max(16, block_size)
if base_size < minimum_size:
self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: base size too small')
# Add enough padding to make it big enough.
# Make a first guess of the padding length.
padding_length_guess = minimum_size - base_size
# See how much larger it should be to make it a multiple of the
# block size.
additional_padding_length = block_size - ((5 + len(data) + padding_length_guess) % block_size)
padding_length = padding_length_guess + additional_padding_length
self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: padding_length=%i', (padding_length,))
self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: cipher=%s', (self.self2remote.cipher.name,))
packet_length = 1 + len(data) + padding_length
self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: packet_length=%i', (packet_length,))
random_padding = ssh.util.random.get_random_data(padding_length)
chunk = struct.pack('>Ic', packet_length, chr(padding_length)) + data + random_padding
self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: chunk_length=%i', (len(chunk),))
mac = self.self2remote.mac.digest(self.self2remote.packet_sequence_number, chunk)
self.self2remote.inc_packet_sequence_number()
self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: mac=%r', (mac,))
#self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: chunk=%r', (chunk,))
encrypted_chunk = self.self2remote.cipher.encrypt(chunk)
#self.debug.write(ssh.util.debug.DEBUG_2, 'send_packet: encrypted_chunk=%r', (encrypted_chunk,))
self.transport.write(encrypted_chunk + mac)
def receive_message(self, wait_till):
"""receive_message(self, wait_till) -> message_type, packet
Read a message off the stream.
<wait_till>: List of message types that you are looking for.
"""
if not self._receive_thread:
raise ssh.transport.SSH_Protocol_Error, 'receive thread not running'
self.tmc.add(coro.current(), wait_till)
try:
return coro._yield()
except:
self.tmc.remove(coro.current())
raise
def start_receive_thread(self):
"""start_receive_thread(self) -> None
Spawns the receive thread.
"""
self.closed = False
self._receive_thread = coro.spawn(self.receive_loop)
def stop_receive_thread(self):
"""stop_receive_thread(self) -> None
Stops the receive thread.
"""
# If the receive loop calls a handler that calls disconnect
# then we don't want to raise an exception on ourself.
if self._receive_thread and self._receive_thread is not coro.current():
self._receive_thread.raise_exception(Stop_Receiving_Exception)
self._receive_thread = None
def receive_loop(self):
"""receive_loop(self) -> None
This is the receive thread. It runs forever processing messages off
the socket.
"""
exc_type = exc_data = exc_tb = None
while 1:
try:
packet, sequence_number = self._receive_packet()
except Stop_Receiving_Exception:
break
except:
exc_type, exc_data, exc_tb = sys.exc_info()
break
if self.ignore_first_packet:
# This can only happen during the beginning of the
# connection, so we don't need to worry about multiple
# threads since there can be only 1.
assert(len(self.tmc.processing_threads)<=1)
self.debug.write(ssh.util.debug.DEBUG_1, 'receive_thread: ignoring first packet')
self.ignore_first_packet = False
continue
message_type = ord(packet[0])
self.debug.write(ssh.util.debug.DEBUG_2, 'receive_thread: message_type=%i', (message_type,))
try:
self._handle_packet(message_type, packet, sequence_number)
except Stop_Receiving_Exception:
break
except:
exc_type, exc_data, exc_tb = sys.exc_info()
break
# XXX: We should check here for SSH_MSG_KEXINIT.
# If we see it, then we should lock down and prevent any
# other messages other than those for the key exchange.
#if message_type == SSH_MSG_KEXINIT:
# Wake up anyone waiting for their message.
if self.tmc.processing_messages.has_key(message_type):
thread = self.tmc.processing_messages[message_type]
try:
self.debug.write(ssh.util.debug.DEBUG_2, 'receive_thread: waiting thread waking up')
coro.schedule(thread, (message_type, packet))
except coro.ScheduleError:
# Coro already scheduled.
pass
self.tmc.remove(thread)
self.closed = True
self.transport.close()
self._receive_thread = None
if exc_data is None:
exc_data = ssh.transport.SSH_Protocol_Error('Receive thread shut down.')
# Wake up anyone still waiting for messages.
for thread in self.tmc.processing_messages.values():
try:
# XXX: Too bad can't pass in traceback.
thread.raise_exception(exc_data, force=False)
except coro.ScheduleError:
# Coro already scheduled.
pass
self.tmc.clear()
def _handle_packet(self, message_type, packet, sequence_number):
if not self.tmc.processing_messages.has_key(message_type):
if self.message_callbacks.has_key(message_type):
f = self.message_callbacks[message_type]
self.debug.write(ssh.util.debug.DEBUG_2, 'receive_thread: calling registered function %s', (f.__name__,))
f(packet)
else:
self.debug.write(ssh.util.debug.DEBUG_2, 'receive_thread: unimplemented message type (%i)', (message_type,))
self.send_unimplemented(sequence_number)
def prepare_keys(self):
self.c2s.cipher.set_encryption_key_and_iv(self.key_exchange.get_encryption_key('C', self.c2s.cipher.key_size),
self.key_exchange.get_encryption_key('A', self.c2s.cipher.iv_size))
self.s2c.cipher.set_encryption_key_and_iv(self.key_exchange.get_encryption_key('D', self.s2c.cipher.key_size),
self.key_exchange.get_encryption_key('B', self.s2c.cipher.iv_size))
self.c2s.mac.set_key(self.key_exchange.get_encryption_key('E', self.c2s.mac.key_size))
self.s2c.mac.set_key(self.key_exchange.get_encryption_key('F', self.s2c.mac.key_size))
def send_newkeys(self):
self.debug.write(ssh.util.debug.DEBUG_3, 'send_newkeys()')
packet = ssh.util.packet.pack_payload(ssh.util.packet.PAYLOAD_MSG_NEWKEYS, (SSH_MSG_NEWKEYS,))
self.send_packet(packet)
# XXX: Unlock key exchange lockdown for self2remote.
def send_unimplemented(self, sequence_number):
self.debug.write(ssh.util.debug.DEBUG_3, 'send_unimplemented(sequence_number=%i)', (sequence_number,))
self.send_packet(
ssh.util.packet.pack_payload(ssh.util.packet.PAYLOAD_MSG_UNIMPLEMENTED,
(SSH_MSG_UNIMPLEMENTED,
sequence_number)
)
)
def _receive_packet(self):
"""_receive_packet(self) -> payload, sequence_number
Reads a packet off the l4 transport.
"""
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet()')
first_chunk = self.transport.read(max(8, self.remote2self.cipher.block_size))
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet: first_chunk=%r', (first_chunk,))
first_chunk = self.remote2self.cipher.decrypt(first_chunk)
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet: post decrypt: %r', (first_chunk,))
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet: cipher=%s', (self.remote2self.cipher.name,))
packet_length = struct.unpack('>I', first_chunk[:4])[0]
min_packet_length = max(16, self.remote2self.cipher.block_size)
# +4 to include the length field.
if packet_length+4 < min_packet_length:
self.debug.write(ssh.util.debug.WARNING, 'receive_packet: packet length too small (len=%i)', (packet_length+4,))
self.send_disconnect(SSH_DISCONNECT_PROTOCOL_ERROR, 'packet length too small: %i' % packet_length)
if packet_length+4 > 1048576: # 1 megabyte
self.debug.write(ssh.util.debug.WARNING, 'receive_packet: packet length too big (len=%i)', (packet_length+4,))
self.send_disconnect(SSH_DISCONNECT_PROTOCOL_ERROR, 'packet length too big: %i' % packet_length)
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet: reading rest of packet (packet_length=%i)', (packet_length,))
rest_of_packet = self.transport.read(packet_length - len(first_chunk) + 4 + self.remote2self.mac.digest_size)
if self.remote2self.mac.digest_size == 0:
mac = ''
else:
mac = rest_of_packet[-self.remote2self.mac.digest_size:]
rest_of_packet = rest_of_packet[:-self.remote2self.mac.digest_size]
rest_of_packet = self.remote2self.cipher.decrypt(rest_of_packet)
packet = first_chunk + rest_of_packet
padding_len = ord(packet[4])
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet: padding_length=%i', (padding_len,))
payload = packet[5:packet_length+4-padding_len]
packet_sequence_number = self.remote2self.packet_sequence_number
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet: packet=%r', (packet,))
self.debug.write(ssh.util.debug.DEBUG_3, 'receive_packet: packet_sequence_number=%i', (packet_sequence_number,))
computed_mac = self.remote2self.mac.digest(packet_sequence_number, packet)
self.remote2self.inc_packet_sequence_number()
if computed_mac != mac:
self.debug.write(ssh.util.debug.WARNING, 'receive_packet: mac did not match: computed=%r actual=%r', (computed_mac, mac))
self.send_disconnect(SSH_DISCONNECT_MAC_ERROR, 'mac did not match')
return payload, packet_sequence_number
def msg_disconnect(self, packet):
msg, reason_code, description, language = ssh.util.packet.unpack_payload(
ssh.util.packet.PAYLOAD_MSG_DISCONNECT, packet)
self.disconnect()
raise ssh.transport.SSH_Protocol_Error, (reason_code, description)
def msg_ignore(self, packet):
#msg, data = ssh.util.packet.unpack_payload(ssh.util.packet.PAYLOAD_MSG_IGNORE, packet)
pass
def msg_debug(self, packet):
msg, always_display, message, language = ssh.util.packet.unpack_payload(
ssh.util.packet.PAYLOAD_MSG_DEBUG, packet)
self.debug.write(ssh.util.debug.DEBUG_1, 'SSH_MSG_DEBUG: %s', message)
def msg_kexinit(self, packet):
self.remote2self.kexinit_packet = packet
msg, cookie, kex_algorithms, server_host_key_algorithms, encryption_algorithms_c2s, \
encryption_algorithms_s2c, mac_algorithms_c2s, mac_algorithms_s2c, \
compression_algorithms_c2s, compression_algorithms_s2c, \
languages_c2s, languages_s2c, first_kex_packet_follows, pad = ssh.util.packet.unpack_payload(
ssh.util.packet.PAYLOAD_MSG_KEXINIT, packet)
self.remote2self.proactive_kex = first_kex_packet_follows
self.c2s.set_supported( kex_algorithms,
server_host_key_algorithms,
encryption_algorithms_c2s,
mac_algorithms_c2s,
compression_algorithms_c2s,
languages_c2s,
1) # Prefer client's list.
self.s2c.set_supported( kex_algorithms,
server_host_key_algorithms,
encryption_algorithms_s2c,
mac_algorithms_s2c,
compression_algorithms_s2c,
languages_s2c,
0) # Prefer client's list.
# The algorithm that we use is the first item that is on the client's
# list that is also on the server's list.
self._matchup_kex_and_key()
self._matchup('cipher')
self._matchup('mac')
self._matchup('compression')
# XXX: lang not supported
# See if we guessed the kex properly.
if self.remote2self.proactive_kex and \
self.remote2self.key_exchange.name != self.key_exchange.name:
# Remote side sent an incorrect initial kex packet...ignore it.
self.ignore_first_packet = True
if self.self2remote.proactive_kex and \
self.self2remote.key_exchange.name != self.key_exchange.name:
# We sent an invalid initial kex packet.
# Resend proper kex packet.
self.debug.write(ssh.util.debug.DEBUG_1, 'msg_kexinit: Resending initial kex packet due to incorrect guess')
if self.is_server:
packet = self.key_exchange.get_initial_server_kex_packet()
else:
packet = self.key_exchange.get_initial_client_kex_packet()
# packet should never be None because if proactive_kex is set,
# then that means we sent the first packet.
assert (packet is not None)
self.send_packet(packet)
# Sync up.
self.remote2self.key_exchange = self.key_exchange
self.self2remote.key_exchange = self.key_exchange
self.remote2self.server_key = self.server_key
self.self2remote.server_key = self.server_key
# Make sure kex algorithm has the information it needs.
self.key_exchange.set_info(self.c2s.version_string, self.s2c.version_string, self.c2s.kexinit_packet, self.s2c.kexinit_packet, self.s2c.supported_server_keys)
def _matchup(self, what):
if getattr(self.remote2self, what) is None:
self.send_disconnect(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'We do not support any of the remote side\'s %ss.' % what)
if getattr(self.self2remote, what) is None:
self.send_disconnect(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'The remote side does not support any of our %ss.' % what)
def _matchup_kex_and_key(self):
"""_matchup_kex_and_key(self) -> None
This sets self.key_exchange and self.server_key to the appropriate
value. It checks that both us and the remote end support the same
key exchange and we both support a key type that has the appropriate
features required by the key exchange algorithm.
"""
self.remote2self.set_preferred('key_exchange')
self.remote2self.set_preferred('server_key')
self.self2remote.set_preferred('key_exchange')
self.self2remote.set_preferred('server_key')
if self.remote2self.key_exchange is None or self.self2remote.key_exchange is None:
self.send_disconnect(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Could not find matching key exchange algorithm.')
if self.remote2self.server_key is None or self.self2remote.server_key is None:
self.send_disconnect(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Could not find matching server key type.')
if self.remote2self.key_exchange.name != self.self2remote.key_exchange.name:
# iterate over client's kex algorithms,
# one at a time. Choose the first algorithm that satisfies
# the following conditions:
# + the server also supports the algorithm,
# + if the algorithm requires an encryption-capable host key,
# there is an encryption-capable algorithm on the server's
# server_host_key_algorithms that is also supported by the
# client, and
# + if the algorithm requires a signature-capable host key,
# there is a signature-capable algorithm on the server's
# server_host_key_algorithms that is also supported by the
# client.
# + If no algorithm satisfying all these conditions can be
# found, the connection fails, and both sides MUST
# disconnect.
for client_kex_algorithm in self.c2s.supported_key_exchanges:
server_kex_algorithm = ssh.util.pick_from_list(client_kex_algorithm.name, self.s2c.supported_key_exchanges)
if server_kex_algorithm is not None:
# We both support this kex algorithm.
# See if we both have key types that match the requirements of this kex algorithm.
for server_host_key_type in self.s2c.supported_server_keys:
if (server_kex_algorithm.wants_encryption_host_key and not server_host_key_type.supports_encryption) or \
(server_kex_algorithm.wants_signature_host_key and not server_host_key_type.supports_signature):
# This host key is not appropriate.
continue
else:
# This key meets our requirements.
break
else:
# None of the host key types worked, try next kex algorithm.
continue
# If we got here, then this is the kex to use.
self.set_key_exchange(client_kex_algorithm.name, server_host_key_type.name)
break
else:
# None of the kex algorithms worked.
self.send_disconnect(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Could not find matching key exchange algorithm.')
else:
# We have agreement on the kex algorithm to use.
# See if we have agreement on the server host key type.
if self.remote2self.server_key.name != self.self2remote.server_key.name:
# See if we share a server host key type that also works with our chosen kex algorithm.
for client_server_key_type in self.c2s.supported_server_keys:
server_server_key_type = ssh.util.pick_from_list(client_server_key_type.name, self.s2c.supported_server_keys)
if server_server_key_type is not None:
# We both support this server key algorithm.
# See if it matches our kex algorithm requirements.
if (self.remote2self.key_exchange.wants_encryption_host_key and not server_server_key_type.supports_encryption) or \
(self.remote2self.key_exchange.wants_signature_host_key and not server_server_key_type.supports_signature):
# This server key type is not appropriate.
continue
else:
# This meets our requirements.
break
else:
# None of the server key types worked.
self.send_disconnect(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Could not find matching server key type for %s key exchange.' % self.remote2self.key_exchange.name)
self.set_key_exchange(self.remote2self.key_exchange.name, self.remote2self.server_key.name)
def set_key_exchange(self, key_exchange=None, server_host_key_type=None):
"""set_key_exchange(self, key_exchange=None, server_host_key_type=None) -> None
Sets the key exchange algorithm to use.
<key_exchange> - A string. The key exchange algorithm to use.
Must be one of those in supported_key_exchanges.
Set to None to default to the preferred algorithm.
<server_host_key_type> - A string. The server host key type to use.
Must be one of thos in supported_server_keys.
Set to None to default to the preferred algorithm.
The key_exchange algorithm MUST support this type
of key.
"""
kex = ssh.util.pick_from_list(key_exchange, self.self2remote.supported_key_exchanges)
if kex is None:
raise ValueError, 'Unknown key exchange algorithm: %s' % key_exchange
key = ssh.util.pick_from_list(server_host_key_type, self.self2remote.supported_server_keys)
if key is None:
raise ValueError, 'Unknown server key type: %s' % server_host_key_type
self.key_exchange = kex
self.server_key = key
if self.is_server:
self.key_exchange.register_server_callbacks()
else:
self.key_exchange.register_client_callbacks()
def _process_kexinit(self):
"""_process_kexinit(self) -> None
This processes the key exchange.
"""
message_type, packet = self.receive_message((SSH_MSG_KEXINIT,))
self.msg_kexinit(packet)
def send_kexinit(self):
"""send_kexinit(self) -> None
Start the key exchange.
"""
# Tell the remote side what features we support.
self.debug.write(ssh.util.debug.DEBUG_3, 'send_kexinit()')
packet = self._send_kexinit()
self.send_packet(packet)
def _send_kexinit(self):
"""_send_kexinit(self) -> None
Sets self2remote.kexinit_packet.
Separate function to help with unittests.
"""
cookie = ssh.util.random.get_random_data(16)
packet = ssh.util.packet.pack_payload(ssh.util.packet.PAYLOAD_MSG_KEXINIT,
(SSH_MSG_KEXINIT,
cookie,
[x.name for x in self.self2remote.supported_key_exchanges],
[x.name for x in self.self2remote.supported_server_keys],
[x.name for x in self.c2s.supported_ciphers],
[x.name for x in self.s2c.supported_ciphers],
[x.name for x in self.c2s.supported_macs],
[x.name for x in self.s2c.supported_macs],
[x.name for x in self.c2s.supported_compressions],
[x.name for x in self.s2c.supported_compressions],
[x.name for x in self.c2s.supported_languages],
[x.name for x in self.s2c.supported_languages],
self.self2remote.proactive_kex, # first_kex_packet_follows
0 # reserved
)
)
self.self2remote.kexinit_packet = packet
return packet
def msg_newkeys(self, packet):
self.debug.write(ssh.util.debug.DEBUG_3, 'msg_newkeys(packet=...)')
# Switch to using new algorithms.
self.remote2self.set_preferred()
self.self2remote.set_preferred()
# Set the keys to use for encryption and MAC.
self.prepare_keys()
self.debug.write(ssh.util.debug.DEBUG_3, 'msg_newkeys: keys have been prepared')
class One_Way_SSH_Transport:
# These are references to the in-use entry from one of the
# supported_xxx lists.
key_exchange = None
server_key = None
compression = None
cipher = None
mac = None
language = None
protocol_version = '2.0'
software_version = 'IronPort_1.0'
comments = ''
version_string = ''
kexinit_packet = ''
# Whether or not we sent our first kex packet with the assumption that
# the remote side supports our preferred algorithms.
proactive_kex = 0
packet_sequence_number = 0L
def __init__(self, transport):
# Instantiate all components.
# An assumption is made that the first (preferred) key exchange algorithm
# supports the first (preferred) server key type.
self.supported_key_exchanges = [Diffie_Hellman_Group1_SHA1(transport)]
self.supported_server_keys = [SSH_DSS(), SSH_RSA()]
self.supported_compressions = [Compression_None()]
self.supported_ciphers = [Triple_DES_CBC(),
Blowfish_CBC(),
Cipher_None(),
]
self.supported_macs = [HMAC_SHA1(),
MAC_None(),
]
self.supported_languages = []
self.set_none()
def inc_packet_sequence_number(self):
"""inc_packet_sequence_number(self) -> None
Raises the packet sequence number by one.
"""
self.packet_sequence_number += 1
if self.packet_sequence_number == 4294967296L:
self.packet_sequence_number = 0
def set_none(self):
"""set_none(self) -> None
Sets the in-use settings to that which is suitable for the beginning
of a connection.
"""
self.key_exchange = None
self.server_key = None
self.compression = Compression_None()
self.cipher = ssh.cipher.none.Cipher_None()
self.mac = MAC_None()
self.language = None
def set_preferred(self, what = None):
"""set_preferred(self, what = None) -> None
Sets the "preferred" pointers to the first element of the appropriate
lists.
<what> - Can be a string to indicate which element to set.
Set to None to set all elements.
"""
def get(list):
try:
return list[0]
except IndexError:
return None
if what is None:
self.key_exchange = get(self.supported_key_exchanges)
self.server_key = get(self.supported_server_keys)
self.compression = get(self.supported_compressions)
self.cipher = get(self.supported_ciphers)
self.mac = get(self.supported_macs)
self.language = get(self.supported_languages)
else:
supported = getattr(self, 'supported_%ss' % what)
setattr(self, what, get(supported))
def set_supported(self, kex, key, encrypt, mac, compress, lang, prefer_self):
"""set_supported(self, kex, key, encrypt, mac, compress, lang, prefer_self) -> None
Sets the supported feature lists.
Each argument is a list of strings.
<prefer_self> - boolean. If true, prefers the order of self.supported_xxx.
If false, prefers the order of the given lists.
"""
def _filter(feature_list, algorithm_list, prefer_self):
algorithm_list[:] = filter(lambda x,y=feature_list: x.name in y, algorithm_list)
if not prefer_self:
# Change the order to match that of <feature_list>
new_list = []
for feature in feature_list:
for self_alg in algorithm_list:
if self_alg.name == feature:
new_list.append(self_alg)
if __debug__:
assert(len(algorithm_list) == len(new_list))
algorithm_list[:] = new_list
_filter(kex, self.supported_key_exchanges, prefer_self)
_filter(key, self.supported_server_keys, prefer_self)
_filter(encrypt, self.supported_ciphers, prefer_self)
_filter(mac, self.supported_macs, prefer_self)
_filter(compress, self.supported_compressions, prefer_self)
_filter(lang, self.supported_languages, prefer_self)
class Thread_Message_Callbacks:
"""Thread_Message_Callbacks()
This is a simple wrapper around the transport's process messages mechanism.
"""
def __init__(self):
# processing_messages: Dictionary of {message: thread}
# Indicates which thread to wake up for each message.
self.processing_messages = {}
# processing_threads: Dictionary of {thread_id:wait_till}
# Reverse of processing_messages.
self.processing_threads = {}
def clear(self):
"""clear(self) -> None
Clear all threads/messages.
"""
self.processing_messages = {}
self.processing_threads = {}
def remove(self, coro_object):
"""remove(self, coro_object) -> None
Remove a thread that is being tracked.
"""
thread_id = coro_object.thread_id()
if self.processing_threads.has_key(thread_id):
messages = self.processing_threads[thread_id]
for m in messages:
assert(self.processing_messages[m] == coro_object)
del self.processing_messages[m]
del self.processing_threads[thread_id]
def add(self, coro_object, messages_waiting_for):
"""add(self, coro_object, messages_waiting_for) -> None
Add a thread to the list of processing.
<coro_object>: The thread object.
<messages_waiting_for>: List of messages the thread is waiting for.
"""
self.processing_threads[coro_object.thread_id()] = messages_waiting_for
for m in messages_waiting_for:
if self.processing_messages.has_key(m):
raise AssertionError, 'Can\'t register message %i with multiple threads.' % m
self.processing_messages[m] = coro_object
class Stop_Receiving_Exception(Exception):
pass
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.util
#
# Utility functions for the SSH code.
#
import string
def pick_from_list(name, algorithms):
"""pick_from_list(name, algorithms) -> algorithm
Picks the algorithm from the list based on the name.
<name> - The name (a string) to find.
<algorithms> - List of algorithm classes that have a "name" attribute.
Returns None if no match found.
If <name> is None, then it will pick the first item in the list.
"""
if name is None:
try:
return algorithms[0]
except IndexError:
return None
for algorithm in algorithms:
if algorithm.name == name:
return algorithm
return None
nonprintable = map(chr, range(256))
nonprintable = filter(lambda x: not x.isprint() and not x.isspace(), nonprintable)
nonprintable = ''.join(nonprintable)
nonprintable_replacement = '$'*len(nonprintable)
nonprintable_table = string.maketrans(nonprintable, nonprintable_replacement)
def safe_string(s):
"""safe_string(s) -> new_s
Escapes control characters in s such that it is suitably
safe for printing to a terminal.
"""
return string.translate(s, nonprintable_table)
def str_xor(a, b):
"""str_xor(a, b) -> str
Returns a^b for every character in <a> and <b>.
<a> and <b> must be strings of equal length.
"""
return ''.join(map(lambda x, y: chr(ord(x) ^ ord(y)), a, b))
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_debug
#
# This module implements the debugging facilities.
#
# Debug information is represented at different levels:
# ERROR - Fatal error.
# WARNING - Warning about a non-fatal problem.
# INFO - General information.
# DEBUG_1 - Action-level information.
# DEBUG_2 - Packet-level information.
# DEBUG_3 - Low-level function tracing.
#
# Setting the log level includes all levels above it.
# Default level is WARNING.
#
ERROR = 0
WARNING = 1
INFO = 2
DEBUG_1 = 3
DEBUG_2 = 4
DEBUG_3 = 5
level_text = {ERROR: 'Error',
WARNING: 'Warning',
INFO: 'Info',
DEBUG_1: 'Debug 1',
DEBUG_2: 'Debug 2',
DEBUG_3: 'Debug 3',
}
import sys
class Debug:
level = WARNING
def write(self, level, message, args=None):
if level <= self.level:
if args is not None:
message = message % args
try:
sys.stderr.write('[%s] %s\n' % (level_text[level], message))
except IOError:
pass
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_mpint
#
# Routines to handle SSH multiple-precision integers.
#
import struct
def pack_mpint(n):
"""pack_mpint(n) -> string
Multiple-Precision Integer
Packs a python long into a big endian string.
"""
# Per the spec:
# - two's compliment
# - big-endian
# - negative numbers have MSB of the first byte set
# - if MSB would be set in a positive number, then preceed with a zero byte
# - Unnecessary leading bytes with the value 0 or 255 MUST NOT be included.
# - Zero stored as four 0 bytes.
# This is very inefficient.
s = []
x = long(n)
if x == 0:
return ''
elif x < 0:
while 1:
s.append(struct.pack('>L', x & 0xffffffffL))
if x == -1:
break
x = x >> 32
else:
while x>0:
s.append(struct.pack('>L', x & 0xffffffffL))
x = x >> 32
s.reverse()
s = ''.join(s)
if s[0]=='\0':
# Remove extra leading zeros.
# This is a positive number.
count = 0
for i in s:
if i != '\0':
break
count += 1
# If the MSB is set, pad with a zero byte.
if ord(s[count])&128:
s = s[count-1:]
else:
s = s[count:]
elif s[0]=='\377' and n < 0:
# Remove extra leading ones.
# This is a negative number.
for x in xrange(len(s)):
i = s[x]
if i != '\377':
break
# If the MSB is not set, then we need to sign-extend and make sure
# there is another byte of all ones.
if ord(s[x])&128:
s = s[x:]
else:
s = s[x-1:]
# If the MSB is set and this is a positive number, pad with a zero.
if n > 0 and ord(s[0])&128:
s = '\0' + s
return s
def unpack_mpint(mpint):
"""unpack_mpint(mpint) -> long
Unpacks a multiple-precision string.
"""
if len(mpint) % 4:
# Need to pad it so that it is a multiple of 4.
pad = 4 - (len(mpint) % 4)
if ord(mpint[0]) & 128:
# Negative number
mpint = '\377' * pad + mpint
struct_format = '>i'
else:
# Positive number
mpint = '\0' * pad + mpint
struct_format = '>I'
else:
if mpint and ord(mpint[0]) & 128:
# Negative
struct_format = '>i'
else:
# Positive
struct_format = '>I'
result = 0L
for x in xrange(0, len(mpint), 4):
result = (result << 32) | struct.unpack(struct_format, mpint[x: x+4])[0]
return result
import unittest
class ssh_packet_test_case(unittest.TestCase):
pass
class mpint_test_case(ssh_packet_test_case):
def runTest(self):
self.check(0, '')
self.check(0x9a378f9b2e332a7L, '\x09\xa3\x78\xf9\xb2\xe3\x32\xa7')
self.check(0x80L, '\0\x80')
self.check(-0x1234L, '\xed\xcc')
self.check(-0xdeadbeefL, '\xff\x21\x52\x41\x11')
self.check(0xffffffffL, '\0\xff\xff\xff\xff')
self.check(-0xffffffffL, '\xff\0\0\0\x01')
self.check(-1L, '\377')
def check(self, num, string):
self.assertEqual(pack_mpint(num), string)
self.assertEqual(unpack_mpint(string), num)
def suite():
suite = unittest.TestSuite()
suite.addTest(mpint_test_case())
return suite
if __name__ == '__main__':
unittest.main(defaultTest='suite')
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_packet
#
# This module implements features to pack and unpack SSH packets.
__version__ = '$Revision: #1 $'
# Format Codes
BYTE = 'byte'
BOOLEAN = 'boolean'
UINT32 = 'uint32'
UINT64 = 'uint64'
STRING = 'string'
MPINT = 'mpint'
NAME_LIST = 'name-list'
FIXED_STRING = 'fixed-string'
import struct
import mpint
import types
def unpack_payload(format, payload, offset=0):
"""unpack_payload(format, payload, offset=0) -> items
Unpacks an SSH payload.
<format> is a list of Format Codes.
<payload> is the SSH payload.
<offset> is the character offset into <payload> to start.
"""
return unpack_payload_get_offset(format, payload, offset)[0]
def unpack_payload_get_offset(format, payload, offset=0):
"""unpack_payload_get_offset(format, payload, offset=0) -> items, index_where_scanning_stopped
Unpacks an SSH payload.
<format> is a list of Format Codes.
<payload> is the SSH payload.
<offset> is the character offset into <payload> to start.
"""
i = offset # Index into payload
result = []
for value_type in format:
if value_type is BYTE:
result.append(payload[i])
i += 1
elif value_type is BOOLEAN:
result.append(ord(payload[i]) and 1 or 0)
i += 1
elif value_type is UINT32:
result.append(struct.unpack('>I', payload[i:i+4])[0])
i += 4
elif value_type is UINT64:
result.append(struct.unpack('>Q', payload[i:i+8])[0])
i += 8
elif value_type is STRING:
str_len = struct.unpack('>I', payload[i:i+4])[0]
i += 4
result.append(payload[i:i+str_len])
i += str_len
elif value_type is MPINT:
mpint_len = struct.unpack('>I', payload[i:i+4])[0]
i += 4
value = payload[i:i+mpint_len]
i += mpint_len
result.append(mpint.unpack_mpint(value))
elif value_type is NAME_LIST:
list_len = struct.unpack('>I', payload[i:i+4])[0]
i += 4
result.append(payload[i:i+list_len].split(','))
i += list_len
elif type(value_type) is types.TupleType:
if value_type[0] is FIXED_STRING:
str_len = value_type[1]
result.append(payload[i:i+str_len])
i += str_len
else:
raise ValueError, value_type
return result, i
def pack_payload(format, values):
"""pack_payload(format, values) -> packet_str
Creates an SSH payload.
<format> is a list Format Codes.
<values> is a tuple of values to use.
"""
packet = [''] * len(format)
if __debug__:
assert(len(values) == len(format))
i = 0
for value_type in format:
if value_type is BYTE:
if type(values[i]) is types.StringType:
if __debug__:
assert(len(values[i]) == 1)
packet[i] = values[i]
else:
packet[i] = chr(values[i])
elif value_type is BOOLEAN:
packet[i] = chr(values[i] and 1 or 0)
elif value_type is UINT32:
packet[i] = struct.pack('>I', values[i])
elif value_type is UINT64:
packet[i] = struct.pack('>Q', values[i])
elif value_type is STRING:
packet[i] = struct.pack('>I', len(values[i])) + values[i]
elif value_type is MPINT:
n = mpint.pack_mpint(values[i])
packet[i] = struct.pack('>I', len(n)) + n
elif value_type is NAME_LIST:
# We could potentially check for validity here.
# Names should be at least 1 byte long and should not
# contain commas.
s = ','.join(values[i])
packet[i] = struct.pack('>I', len(s)) + s
elif type(value_type) is types.TupleType and value_type[0] is FIXED_STRING:
packet[i] = values[i]
else:
raise ValueError, value_type
i += 1
return ''.join(packet)
# Packet format definitions.
PAYLOAD_MSG_DISCONNECT = (BYTE,
UINT32, # reason code
STRING, # description
STRING) # language tag
PAYLOAD_MSG_IGNORE = (BYTE,
STRING) # data
PAYLOAD_MSG_UNIMPLEMENTED = (BYTE,
UINT32) # packet sequence number of rejected message
PAYLOAD_MSG_DEBUG = (BYTE,
BOOLEAN, # always_display
STRING, # message
STRING) # language tag
PAYLOAD_MSG_KEXINIT = (BYTE,
(FIXED_STRING, 16),# cookie
NAME_LIST, # kex_algorithms
NAME_LIST, # server_host_key_algorithms
NAME_LIST, # encryption_algorithms_client_to_server
NAME_LIST, # encryption_algorithms_server_to_client
NAME_LIST, # mac_algorithms_client_to_server
NAME_LIST, # mac_algorithms_server_to_client
NAME_LIST, # compression_algorithms_client_to_server
NAME_LIST, # compression_algorithms_server_to_client
NAME_LIST, # languages_client_to_server
NAME_LIST, # languages_server_to_client
BOOLEAN, # first_kex_packet_follows
UINT32) # 0 (reserved for future extension)
PAYLOAD_MSG_NEWKEYS = (BYTE,)
PAYLOAD_MSG_SERVICE_REQUEST = (BYTE,
STRING) # service name
PAYLOAD_MSG_SERVICE_ACCEPT = (BYTE,
STRING) # service_name
import unittest
class ssh_packet_test_case(unittest.TestCase):
pass
class unpack_test_case(ssh_packet_test_case):
def runTest(self):
# KEXINIT packet grabbed from my OpenSSH server.
sample_packet = '\024\212a\330\261\300}.\252b%~\006j\242\356\367\000\000\000=diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1\000\000\000\007ssh-dss\000\000\000\207aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,arcfour,aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se,aes128-ctr,aes192-ctr,aes256-ctr\000\000\000\207aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,arcfour,aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se,aes128-ctr,aes192-ctr,aes256-ctr\000\000\000Uhmac-md5,hmac-sha1,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96\000\000\000Uhmac-md5,hmac-sha1,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96\000\000\000\011none,zlib\000\000\000\011none,zlib\000\000\000\000\000\000\000\000\000\000\000\000\000'
msg, cookie, kex_algorithms, server_host_key_algorithms, encryption_algorithms_c2s, \
encryption_algorithms_s2c, mac_algorithms_c2s, mac_algorithms_s2c, \
compression_algorithms_c2s, compression_algorithms_s2c, \
languages_c2s, languages_s2c, first_kex_packet_follows, reserved = unpack_payload(
PAYLOAD_MSG_KEXINIT, sample_packet)
def suite():
suite = unittest.TestSuite()
suite.addTest(unpack_test_case())
return suite
if __name__ == '__main__':
unittest.main(module='ssh_packet', defaultTest='suite')
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.util.password
#
# Simple code to input a password.
#
import termios
import sys
def get_password(prompt):
print prompt,
# Turn off echo.
term_settings = termios.tcgetattr(0) # 0 is stdin
term_settings[3] &= ~termios.ECHO
termios.tcsetattr(0, termios.TCSANOW, term_settings)
try:
result = sys.stdin.readline()
finally:
# Restore ECHO
term_settings[3] |= termios.ECHO
termios.tcsetattr(0, termios.TCSANOW, term_settings)
if not result:
raise EOFError
return result[:-1] # Strip trailing newline
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh_random
#
# Consolidated location for getting random data.
#
import Crypto.Util.number
import Crypto.Util.randpool
import math
# XXX: Should we stir the pool occasionally?
random_pool = Crypto.Util.randpool.RandomPool()
def get_random_data(bytes):
"""get_random_data(bytes) -> str
Gets <bytes> number of bytes of random data.
"""
return random_pool.get_bytes(bytes)
def get_random_number(bits):
"""get_random_number(bits) -> python long
Return a random number.
<bits> is the number of bits in the number.
"""
return Crypto.Util.number.getRandomNumber(bits, random_pool.get_bytes)
def get_random_number_from_range(low, high):
"""get_random_number_from_range(low, high) -> python long
Returns a random number that is greater than <low> and less than <high>.
"""
bits = highest_bit(high)
while 1:
x = get_random_number(bits)
if x > low and x < high:
break
return x
def highest_bit(num):
"""highest_bit(num) -> n
Determines the bit position of the highest bit in the given number.
Example: 0x800000 returns 24.
"""
try:
return int(math.floor(math.log10(num) / math.log10(2))) + 1
except OverflowError:
assert(num==0)
return 0
# This was an alternate version of highest_bit, but the
# logarithm version seems to be a little faster.
highest_bit_map = {
'0' : 0,
'1' : 1,
'2' : 2,
'3' : 2,
'4' : 3,
'5' : 3,
'6' : 3,
'7' : 3,
'8' : 4,
'9' : 4,
'A' : 4,
'B' : 4,
'C' : 4,
'D' : 4,
'E' : 4,
'F' : 4,
}
def highest_bit2(num):
"""highest_bit(num) -> n
Determines the bit position of the highest bit in the given number.
Example: 0x800000 returns 24.
"""
# Use a table for the high nibble, then count the number
# of nibbles up to the highest one. This algorithm is significantly faster
# than shifting the number until it is zero.
# Convert to long so that we don't have to test if it is an int or long
# in order to accomodate for L at the end.
h = hex(long(num))
# Subtract 4 to accomodate for first two characters (0x) the last
# character (L) and the high nibble character.
return (len(h) - 4)*4 + highest_bit_map[h[2]]
import unittest
class ssh_random_test_case(unittest.TestCase):
pass
class highest_bit_test_case(ssh_random_test_case):
def runTest(self):
for x in xrange(300):
self.assertEqual(highest_bit(x), highest_bit2(x))
x = 144819228510396375480510966045726324197234443151241728654670685625305230385467763734653299992854300412367868856607501321634131298084648429649714452472261648519166487595581105734370788168033696455943547609540069712392591019911289209306656760054646817215504894551439102079913490941604156000063251698742214491563L
self.assertEqual(highest_bit(x), highest_bit2(x))
while x > 0:
x = x/2
self.assertEqual(highest_bit(x), highest_bit2(x))
def suite():
suite = unittest.TestSuite()
suite.addTest(highest_bit_test_case())
return suite
if __name__ == '__main__':
unittest.main(module='ssh_random', defaultTest='suite')
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Copyright (c) 2002-2012 IronPort Systems and Cisco Systems
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ssh.wrapper.coro_interactive_ssh_wrapper
#
# This is a easy-to-use wrapper that uses coro_ssh.
#
# It has only a few features, but many may be added in the future.
import dnsqr
import inet_utils
import ssh.transport.client
import ssh.connection.connect
import ssh.l4_transport.coro_socket_transport
import ssh.auth.userauth
import ssh.connection.interactive_session
import ssh.util.debug
DISABLE_PASSWORD = '__DISABLE_PASSWORD__'
class Coro_Interactive_SSH_Wrapper:
client = None
transport = None
service = None
channel = None
def __init__(self):
pass
def connect(self, username, remote_address, remote_port=22, password=None, command=None, debug_level=ssh.util.debug.WARNING):
"""connect(self, username, remote_address, remote_port=22, password=None, command=None, debug_level=ssh.util.debug.WARNING) -> None
The opens a connection to the remote side and authenticates.
<username> - The remote username to log into.
<remote_address> - The remote address to connect to.
<remote_port> - The remote port to connect to.
<password> - The password to use when connecting to the remote side.
If None, and there are no authorized_keys configured,
then it will ask for the password on stdout/stdin.
If DISABLE_PASSWORD, will disable password auth.
<command> - The command to run on the remote side.
If no command is given, then it will open a pty and shell.
<debug_level> - Level a debuging to print to stderr.
"""
self.client = ssh.transport.client.SSH_Client_Transport()
if inet_utils.is_ip(remote_address):
remote_ip = remote_address
hostname = None
else:
dns_query_result = dnsqr.query(remote_address, 'A')
remote_ip = dns_query_result[0][-1]
hostname = remote_address
coro_socket_transport = ssh.l4_transport.coro_socket_transport
self.transport = coro_socket_transport.coro_socket_transport(
remote_ip, remote_port, hostname=hostname)
self.client.connect(self.transport)
self.client.debug.level = debug_level
self.service = ssh.connection.connect.Connection_Service(self.client)
self._authenticate(username, password)
self.channel = ssh.connection.interactive_session.Interactive_Session_Client(self.service)
self.channel.open()
if command is not None:
self.channel.exec_command(command)
else:
self.channel.open_pty()
self.channel.open_shell()
def _authenticate(self, username, password=None):
auth_method = ssh.auth.userauth.Userauth(self.client)
auth_method.username = username
if password is not None:
for x in xrange(len(auth_method.methods)):
if auth_method.methods[x].name == 'password':
break
else:
# This should never happen.
raise ValueError, 'Expected password auth method in Userauth class'
if password is DISABLE_PASSWORD:
del auth_method.methods[x]
else:
password_method = Fixed_Password_Auth(self.client)
password_method.password = password
auth_method.methods[x] = password_method
self.client.authenticate(auth_method, self.service.name)
def disconnect(self):
if self.channel is not None:
self.channel.close()
if self.client is not None:
self.client.disconnect()
close = disconnect
def read(self, bytes):
return self.channel.read(bytes)
recv = read
def read_exact(self, bytes):
return self.channel.read_exact(bytes)
def write(self, data):
self.channel.send(data)
send = write
class Fixed_Password_Auth(ssh.auth.userauth.Password):
password = None
def get_password(self, username, prompt=None):
return self.password
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment