April 17, 2015

SSSHHH! Testing Python SSH client with auth via private key

This post is about unit testing SSH clients written in Python. The key point is usage of public and private RSA keys for authentication. There are some docs, articles and examples on the web, but most of them are using username/password for doing that. You may dig around paramiko's test cases and examples, you may find 'MockSSH' interesting, you may have already heard aboud Twisted's 'conch' and 'cred' subsystems. But if you are going to find answers for some real-life questions in the docs then you are welcome to the wonderland! Yay! And if you decide to look into the sources of that stuff then you are on a highway to hell for sure. So, I hope this post may somehow help someone someday.

First of all, let's talk about dependencies. We are going to test simple SSH client and 'paramiko' library will help us to implement a lite one. Next, we will need to implement a lightweight SSH server which can be run on demand and which our client can connect to. To do this we'll use 'Twisted' framework. Finally, we will need to run our tests. I'm quite lazy and I'm going to run them via 'nose' tool. So, here's our final list of dependencies:

Looks good. Let's install them:

Now we need to create public and private RSA keys for testing purpose. 'ssh-keygen' is our choice for this:

In this example keys are located in current directory and their names are 'user.key' and 'user.key.pub'. Private key has no password to keep example simple. If you are too lazy to create keys by our own, you can download mine:

Now we are ready to strike. What should we do? We need to test our keys. Let's create our SSH server for this. Oops. I've already done it. Nice of me, innit? You are welcome to download it:

You do not need to look inside 'mock_server.py' at this moment. Now you can run it. Give it a try:

By default it will run at 'localhost:2222', it will know about existence of user 'user', and it will look for corresponding keys in current directory. Pass '--help' argument to it to see how you can change that values.

Now it's time to connect to our server. Firstly, let's make our private key visible for ssh-agent:

We are ready to connect, so let's do this:

We can see a kind greeting for the server. Let's try to talk with him:

Okay, well, now you have a machine gun. HO-HO-HO. Our server can process some commands. Now we are sure we have some endpoint to connect to and we use private key for authentication.

If you are still reading this, you may ask: "Hey, so what about promised unit testing? How can daemon help us?" and you'll be right. Actually, we do not need to run mock daemon from console for unit testing. We'll launch mock SSH server during tests in a separate thread, but it still may be useful for you to run console daemon for some reason. Who knows?

So, let's start to write our unit tests by defining imports and so on:

In addition to console daemon, 'mock_server' module provides ability to start and stop server in a thread by demand. We are going to start and stop server before and after execution of test case respectively:

'start_threaded_server' method accepts absolutely the same parameters as console daemon: interface, port, username and path to directory with keys.

We are ready to create and delete SSH client for each separate test:

Note, that we use private key to connect to server. We will use 'exec_command' method of paramiko's client to run commands on server:

Lazy people can download this module ;)

And then execute tests:

This point may be considered as the end of this article. Congratulazioni!

There are still few words I need to say. First of all, handlers of commands which are sent from console and handlers of commands which are sent via paramiko's 'exec_command' are quite different things. Former are invoked within 'SSHMockProtocol.lineReceived' method and they are easy to use and understand:

Latter are invoked within 'SSHMockAvatar.execCommand' method:

This part is not so trivial and it demands some special magic to be done. Note, that 'protocol' argument is an instance of 'twisted.conch.ssh.session.SSHSessionProcessProtocol' which in general is used to run some subprocesses via Twisted's reactor.

Please, tell in comments, if there is any better or elegant approach.

It may be a nice homework task for you to update example so that you can use private key with password also. Another good thing to think about is multiuser support.

Good luck and have a nice day!