Python - pysftp / paramiko - Verify host key using its fingerprint
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 __eq__
method (or __cmp__
before Paramiko 3.0) 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.
Verify host key with pysftp against known_hosts file with custom port
I guess the problem is that pysftp do not support custom ports when looking up the host key in the known_hosts
file.
Add an entry like this:
data-nz.metservice.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8N65MCdnbHjaEDxkZPPq1QO0RLwP3cdm9Gb9BAMS0mFH39d7/yHIerA6yFZRW27u3NClI7V1F3hDuheoCUomeF9Q9ioaeQ2dlX27hmGf611RpSfI/vGgnmipHYzzHsCIJi0LxuowCouKNw8g1v1e2VzsVWFPaq+cDeuUpDwpBKWnxQUWN7U9mzN1k0sDALimWOzhfQmXtCzPkHqERUcPpdU7/zWP8Xk9H7FQxgiPFa+EC5xuCzn01CcJppQ8VBqL9R6SNNP/d9ymQWh3cotXe6sj5gt2MdfbAUfxddQITW1rU+LSOkG21QPMq0VBDJwWf9RpqhnqcvusZIFVGyOsn
Or use Paramiko directly (pysftp is a wrapper around Paramiko), as Paramiko implements the host key lookup with custom port correctly.
Some background: The codes at the beginning of your original known_hosts
are hashed host[:port]
identifiers. Pysftp does not use port when looking up the entry, so it does not find the correct one. If you add fake (hashed or not) entry without the custom port, pysftp will be able to find it.
I can't connect to a server with pysftp
You can debug this in following ways:
- Check if your key have any white spaces at the beginning or at the end.
- Then ssh to that server with your key directly from
terminal make sure you use-v
option to verbose logs. - Check
/etc/ssh/sshd_config
if sftp is allowed or not (good approach is to have diffrent sftp user for secure communication).
Thanks
Connecting to an SFTP server using pysftp and Python 3 with just the server fingerprint
Solution can be found at Python - pysftp / paramiko - Verify host key using its fingerprint but I had to alter it a bit to use Python 3.
import hashlib as hl
def trim_fingerprint(fingerprint):
#if fingerprint.startswith('ecdsa-sha2-nistp384 384 '):
#return fingerprint[len('ecdsa-sha2-nistp384 384 '):]
return fingerprint
def clean_fingerprint(fingerprint):
#return trim_fingerprint(fingerprint).replace(':', '')
return trim_fingerprint(fingerprint)
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 hl.md5(other).digest().encode('hex') == self.fingerprint:
#The line below is required for Python 3. Above is Python 2.
elif hl.md5(other).hexdigest() == 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'ecdsa-sha2-nistp384'
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
I had to manually remove any ":" from the fingerprint because it just never worked allowing the script to do it.
Usage:
options = pysftp.CnOpts()
options.hostkeys.clear()
options.hostkeys.add('www.sample.com', u'ecdsa-sha2-nistp384 384 ', AuthOnFingerPrint.FingerprintKey(serverkey))
Where serverkey is the finger print.
How to set up a hostkey file using ssh-ed25519 as a key for pysftp
What goes to the "host key file" is a full host/public key, not only fingerprint.
For details, see (again):
Verify host key with pysftp
If you want to verify the host key using it's fingerprint only, see:
Python - pysftp / paramiko - Verify host key using its fingerprint
(though that' linked in the first answer already anyway).
Using Python's pysftp, how do you verify a host key?
cnopts = pysftp.CnOpts()
cnopts.hostkeys.load('sftpserver.pub')
where the sftpserver.pub
contains a server public key in a format like:
example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB...
An easy way to retrieve the host key in this format is using OpenSSH ssh-keyscan
:
ssh-keyscan example.com
Though for absolute security, you should not retrieve the host key remotely, as you cannot be sure, if you are not being attacked already.
See my article Where do I get SSH host key fingerprint to authorize the server? It's for my WinSCP SFTP client, but most information there is valid in general.
If you do not want to use an external file, you can also use
cnopts.hostkeys.add(...)
For other options, see: Verify host key with pysftp.
Though pysftp is dead. Better use Paramiko: pysftp vs. Paramiko
Related Topics
How to Fix "Importerror: Dll Load Failed" While Importing Win32Api
How to Find Unused Functions in Python Code
How to Open Multiple Webpages in Separate Tabs Within a Browser Using Selenium-Webdriver and Python
In Python, How Does One Catch Warnings as If They Were Exceptions
Rect Collision with List of Rects
Create a Main Loop with Tkinter
What Is the Most Efficient Way of Counting Occurrences in Pandas
How to Leave/Exit/Deactivate a Python Virtualenv
Correct Style for Python Functions That Mutate the Argument
Reference Requirements.Txt for the Install_Requires Kwarg in Setuptools Setup.Py File
Anyone Know of a Good Python Based Web Crawler That I Could Use
Python: Use MySQLdb to Import a MySQL Table as a Dictionary
SchröDinger's Variable: the _Class_ Cell Magically Appears If You'Re Checking for Its Presence
How to Do N-D Distance and Nearest Neighbor Calculations on Numpy Arrays
Select Multiple Ranges of Columns in Pandas Dataframe