Python - pysftp / paramiko - 使用指纹验证主机密钥
Python - pysftp / paramiko - Verify host key using its fingerprint
此代码抛出异常。如何在不将 SSH 指纹存储在文件中的情况下验证它?我相信下面的代码是为 public 键设计的。但是带有 SFTP 服务器的客户端验证了指纹,但没有给我 public 密钥。
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, "/")
代码基于Verify host key with pysftp.
根据您的需要,您可以使用以下两种方法之一:
如果您只需要验证一个特定的主机密钥
使用 ssh-keyscan
(或类似方法)检索主机 public 密钥:
ssh-keyscan example.com > tmp.pub
tmp.pub
看起来像(known_hosts
文件格式):
example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0hVqZOvZ7yWgie9OHdTORJVI5fJJoH1yEGamAd5G3werH0z7e9ybtq1mGUeRkJtea7bzru0ISR0EZ9HIONoGYrDmI7S+BiwpDBUKjva4mAsvzzvsy6Ogy/apkxm6Kbcml8u4wjxaOw3NKzKqeBvR3pc+nQVA+SJUZq8D2XBRd4EDUFXeLzwqwen9G7gSLGB1hJkSuRtGRfOHbLUuCKNR8RV82i3JvlSnAwb3MwN0m3WGdlJA8J+5YAg4e6JgSKrsCObZK7W1R6iuyuH1zA+dtAHyDyYVHB4FnYZPL0hgz2PSb9c+iDEiFcT/lT4/dQ+kRW6DYn66lS8peS8zCJ9CSQ==
现在,您可以计算 public 密钥的指纹 ssh-keygen
:
ssh-keygen -l -f tmp.pub -E md5
(仅将 -E md5
用于支持多种指纹算法且默认为 SHA256 的较新版本的 OpenSSH)
你会得到类似的东西:
2048 MD5:c4:26:18:cf:a0:15:9a:5f:f3:bf:96:d8:3b:19:ef:7b example.com (RSA)
如果指纹与您的指纹匹配,您现在可以安全地假设 tmp.pub
是合法的 public 密钥并在代码中使用它:
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:
(基于Verify host key with pysftp)
如果您需要根据指纹自动验证主机密钥
例如因为指纹来自外部配置。
我不确定 pysftp 的有限 API 是否允许这样做。您可能不得不跳过 pysftp 并直接使用 Paramiko library(pysftp 在内部使用 Paramiko)。
有了Paramiko,就可以巧妙的实现MissingHostKeyPolicy
interface.
从 AutoAddPolicy
的实现方式开始:
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())))
请注意,在代码中,您可以在 hexlify(key.get_fingerprint())
中使用指纹。只需将该值与您拥有的指纹进行比较即可。如果匹配,就 return。否则引发异常,
就像 RejectPolicy
那样。
另一个解决方案(即使与 pysftp 一起工作)是实现 PKey
in a way that it holds only the fingerprint. And implement its __cmp__
method 以仅比较指纹。然后可以将 PKey
的此类实例添加到 cnopts.hostkeys.add
.
OP 在 . Allegedly for Python 3, more complex implementation is needed, as seen in .
中发布了此方法的实现
根据 Martin Prikryl 的回答,以下是我的解决方案。
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
用法:
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')
此代码抛出异常。如何在不将 SSH 指纹存储在文件中的情况下验证它?我相信下面的代码是为 public 键设计的。但是带有 SFTP 服务器的客户端验证了指纹,但没有给我 public 密钥。
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, "/")
代码基于Verify host key with pysftp.
根据您的需要,您可以使用以下两种方法之一:
如果您只需要验证一个特定的主机密钥
使用 ssh-keyscan
(或类似方法)检索主机 public 密钥:
ssh-keyscan example.com > tmp.pub
tmp.pub
看起来像(known_hosts
文件格式):
example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0hVqZOvZ7yWgie9OHdTORJVI5fJJoH1yEGamAd5G3werH0z7e9ybtq1mGUeRkJtea7bzru0ISR0EZ9HIONoGYrDmI7S+BiwpDBUKjva4mAsvzzvsy6Ogy/apkxm6Kbcml8u4wjxaOw3NKzKqeBvR3pc+nQVA+SJUZq8D2XBRd4EDUFXeLzwqwen9G7gSLGB1hJkSuRtGRfOHbLUuCKNR8RV82i3JvlSnAwb3MwN0m3WGdlJA8J+5YAg4e6JgSKrsCObZK7W1R6iuyuH1zA+dtAHyDyYVHB4FnYZPL0hgz2PSb9c+iDEiFcT/lT4/dQ+kRW6DYn66lS8peS8zCJ9CSQ==
现在,您可以计算 public 密钥的指纹 ssh-keygen
:
ssh-keygen -l -f tmp.pub -E md5
(仅将 -E md5
用于支持多种指纹算法且默认为 SHA256 的较新版本的 OpenSSH)
你会得到类似的东西:
2048 MD5:c4:26:18:cf:a0:15:9a:5f:f3:bf:96:d8:3b:19:ef:7b example.com (RSA)
如果指纹与您的指纹匹配,您现在可以安全地假设 tmp.pub
是合法的 public 密钥并在代码中使用它:
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:
(基于Verify host key with pysftp)
如果您需要根据指纹自动验证主机密钥
例如因为指纹来自外部配置。
我不确定 pysftp 的有限 API 是否允许这样做。您可能不得不跳过 pysftp 并直接使用 Paramiko library(pysftp 在内部使用 Paramiko)。
有了Paramiko,就可以巧妙的实现MissingHostKeyPolicy
interface.
从 AutoAddPolicy
的实现方式开始:
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())))
请注意,在代码中,您可以在 hexlify(key.get_fingerprint())
中使用指纹。只需将该值与您拥有的指纹进行比较即可。如果匹配,就 return。否则引发异常,
就像 RejectPolicy
那样。
另一个解决方案(即使与 pysftp 一起工作)是实现 PKey
in a way that it holds only the fingerprint. And implement its __cmp__
method 以仅比较指纹。然后可以将 PKey
的此类实例添加到 cnopts.hostkeys.add
.
OP 在
根据 Martin Prikryl 的回答,以下是我的解决方案。
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
用法:
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')