Python - pysftp / paramiko - Verify host key using its fingerprint

12,381

Solution 1

Depending on your needs you can use either of these two methods:

In case you need to verify only one specific host key

Use ssh-keyscan (or similar) to retrieve the host public key:

ssh-keyscan example.com > tmp.pub

The tmp.pub will look like (known_hosts file format):

example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0hVqZOvZ7yWgie9OHdTORJVI5fJJoH1yEGamAd5G3werH0z7e9ybtq1mGUeRkJtea7bzru0ISR0EZ9HIONoGYrDmI7S+BiwpDBUKjva4mAsvzzvsy6Ogy/apkxm6Kbcml8u4wjxaOw3NKzKqeBvR3pc+nQVA+SJUZq8D2XBRd4EDUFXeLzwqwen9G7gSLGB1hJkSuRtGRfOHbLUuCKNR8RV82i3JvlSnAwb3MwN0m3WGdlJA8J+5YAg4e6JgSKrsCObZK7W1R6iuyuH1zA+dtAHyDyYVHB4FnYZPL0hgz2PSb9c+iDEiFcT/lT4/dQ+kRW6DYn66lS8peS8zCJ9CSQ==

Now, you can calculate a fingerprint of that public key with ssh-keygen:

ssh-keygen -l -f tmp.pub -E md5

(use the -E md5 only with newer versions of OpenSSH that support multiple fingerprint algorithms and default to SHA256)

You will get something like:

2048 MD5:c4:26:18:cf:a0:15:9a:5f:f3:bf:96:d8:3b:19:ef:7b example.com (RSA)

If the fingerprint matches with the one you have, you can now safely assume that the tmp.pub is a legitimate public key and use it in the code:

from base64 import decodebytes
# ...

keydata = b"""AAAAB3NzaC1yc2EAAAABIwAAAQEA0hV..."""
key = paramiko.RSAKey(data=decodebytes(keydata))
cnopts = pysftp.CnOpts()
cnopts.hostkeys.add('example.com', 'ssh-rsa', key)

with pysftp.Connection(host, username, password, cnopts=cnopts) as sftp:

(based on Verify host key with pysftp)

In case you need to automate verification of a host key based on its fingerprint

E.g. because the fingerprint comes from an external configuration.

I'm not sure if a limited API of pysftp allows that. You probably would have to skip pysftp and use Paramiko library directly (pysftp uses Paramiko internally).

With Paramiko, you can cleverly implement MissingHostKeyPolicy interface.

Start with how AutoAddPolicy is implemented:

class AutoAddPolicy (MissingHostKeyPolicy):
    """
    Policy for automatically adding the hostname and new host key to the
    local `.HostKeys` object, and saving it.  This is used by `.SSHClient`.
    """

    def missing_host_key(self, client, hostname, key):
        client._host_keys.add(hostname, key.get_name(), key)
        if client._host_keys_filename is not None:
            client.save_host_keys(client._host_keys_filename)
        client._log(DEBUG, 'Adding %s host key for %s: %s' %
                    (key.get_name(), hostname, hexlify(key.get_fingerprint())))

Note that in the code you have the fingerprint available in hexlify(key.get_fingerprint()). Just compare that value against the fingerprint you have. If it matches, just return. Otherwise raise an exception, like the RejectPolicy does.


Another solution (which would work even with pysftp) is to implement PKey in a way that it holds only the fingerprint. And implement its __cmp__ method to compare the fingerprint only. Such an instance of PKey can then be added to cnopts.hostkeys.add.

OP posted an implementation of this approach in his answer. Allegedly for Python 3, more complex implementation is needed, as seen in Connecting to an SFTP server using pysftp and Python 3 with just the server fingerprint.

Solution 2

Based on Martin Prikryl's answer, below is my solution.

def trim_fingerprint(fingerprint):
    if fingerprint.startswith('ssh-rsa 2048 '):
        return fingerprint[len('ssh-rsa 2048 '):]
    return fingerprint
def clean_fingerprint(fingerprint):
    return trim_fingerprint(fingerprint).replace(':', '')

class FingerprintKey:
    def __init__(self, fingerprint):
        self.fingerprint = clean_fingerprint(fingerprint)
    def compare(self, other):
        if callable(getattr(other, "get_fingerprint", None)):
            return other.get_fingerprint() == self.fingerprint
        elif clean_fingerprint(other) == self.get_fingerprint():
            return True
        elif md5(other).digest().encode('hex') == self.fingerprint:
            return True
        else:
            return False
    def __cmp__(self, other):
        return self.compare(other)
    def __contains__(self, other):
        return self.compare(other)
    def __eq__(self, other):
        return self.compare(other)
    def __ne__(self, other):
        return not self.compare(other)
    def get_fingerprint(self):
        return self.fingerprint
    def get_name(self):
        return u'ssh-rsa'
    def asbytes(self):
         # Note: This returns itself.
         #   That way when comparisons are done to asbytes return value,
         #   this class can handle the comparison.
        return self

usage:

options = pysftp.CnOpts()
options.hostkeys.clear()
options.hostkeys.add('www.example.com', u'ssh-rsa', FingerprintKey("ssh-rsa 2048 d8:4e:f1:f1:f1:f1:f1:f1:21:31:41:14:13:12:11:aa"))
    with pysftp.Connection(
                    'www.example.com',
                    username='user',
                    password='password',
                    cnopts=options) as conn:
        conn.get_d('remote/filedir', 'c:/local/output')
Share:
12,381
TamusJRoyce
Author by

TamusJRoyce

Started programming around the age of 11 (qbasic + shift F1 for help). 27 now. Plus some years. At least 12. Love billiards and use to be good at it. I like Neural Networks, FSM's, DataStructures, embedded systems such as ESP32, risc-v, Raspberry Pi... I am back to being a .NET programmer with some node and Angular+ thrown in. I am transitioning away from embedded IoT in my professional career. But it will never leave my personal hobbies! Remix (react), next.js (react), svelte, deno, blazor.net are front-end interests. Currently work in Angular. Azure technologies, including containers.

Updated on June 15, 2022

Comments

  • TamusJRoyce
    TamusJRoyce almost 2 years

    This code throws an exception. How can I verify an SSH fingerprint without storing it in a file? I believe the code below is designed for a public key. But the client with the SFTP server validated the fingerprint and did not get me the public key.

    import os
    import shutil
    
    import pysftp
    import paramiko
    
    connection_info = {
        'server': "example.com",
        'user': "user",
        'passwd': "password",
        'target_dir': "out/prod",
        'hostkey': "ssh-rsa 2048 d8:4e:f1:f1:f1:f1:f1:f1:21:31:41:14:13:12:11:aa",
    }
    
    def move_files_from_server_to_local(server, localpath):
        target_dir = server['target_dir']
        keydata = "d8:4e:f1:f1:f1:f1:f1:f1:21:31:41:14:13:12:11:aa"
        key = paramiko.RSAKey(data=decodebytes(keydata))
        options = pysftp.CnOpts()
        options.hostkeys.add('example.com', 'ssh-rsa', key)
        with pysftp.Connection(
                        server['server'],
                        username=server['user'],
                        password=server['passwd'],
                        cnopts=options) as conn:
            conn.get_d(target_dir, localpath)
            delete_files_from_dir(conn, target_dir)
    
    move_files_from_server_to_local(connection_info, "/")
    

    The code is based on Verify host key with pysftp.