hostkeys.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. # Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>
  2. #
  3. # This file is part of paramiko.
  4. #
  5. # Paramiko is free software; you can redistribute it and/or modify it under the
  6. # terms of the GNU Lesser General Public License as published by the Free
  7. # Software Foundation; either version 2.1 of the License, or (at your option)
  8. # any later version.
  9. #
  10. # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
  11. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  13. # details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
  17. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  18. import binascii
  19. import os
  20. import sys
  21. if sys.version_info[:2] >= (3, 3):
  22. from collections.abc import MutableMapping
  23. else:
  24. from collections import MutableMapping
  25. from hashlib import sha1
  26. from hmac import HMAC
  27. from paramiko.py3compat import b, u, encodebytes, decodebytes
  28. from paramiko.dsskey import DSSKey
  29. from paramiko.rsakey import RSAKey
  30. from paramiko.util import get_logger, constant_time_bytes_eq
  31. from paramiko.ecdsakey import ECDSAKey
  32. from paramiko.ed25519key import Ed25519Key
  33. from paramiko.ssh_exception import SSHException
  34. class HostKeys(MutableMapping):
  35. """
  36. Representation of an OpenSSH-style "known hosts" file. Host keys can be
  37. read from one or more files, and then individual hosts can be looked up to
  38. verify server keys during SSH negotiation.
  39. A `.HostKeys` object can be treated like a dict; any dict lookup is
  40. equivalent to calling `lookup`.
  41. .. versionadded:: 1.5.3
  42. """
  43. def __init__(self, filename=None):
  44. """
  45. Create a new HostKeys object, optionally loading keys from an OpenSSH
  46. style host-key file.
  47. :param str filename: filename to load host keys from, or ``None``
  48. """
  49. # emulate a dict of { hostname: { keytype: PKey } }
  50. self._entries = []
  51. if filename is not None:
  52. self.load(filename)
  53. def add(self, hostname, keytype, key):
  54. """
  55. Add a host key entry to the table. Any existing entry for a
  56. ``(hostname, keytype)`` pair will be replaced.
  57. :param str hostname: the hostname (or IP) to add
  58. :param str keytype: key type (``"ssh-rsa"`` or ``"ssh-dss"``)
  59. :param .PKey key: the key to add
  60. """
  61. for e in self._entries:
  62. if (hostname in e.hostnames) and (e.key.get_name() == keytype):
  63. e.key = key
  64. return
  65. self._entries.append(HostKeyEntry([hostname], key))
  66. def load(self, filename):
  67. """
  68. Read a file of known SSH host keys, in the format used by OpenSSH.
  69. This type of file unfortunately doesn't exist on Windows, but on
  70. posix, it will usually be stored in
  71. ``os.path.expanduser("~/.ssh/known_hosts")``.
  72. If this method is called multiple times, the host keys are merged,
  73. not cleared. So multiple calls to `load` will just call `add`,
  74. replacing any existing entries and adding new ones.
  75. :param str filename: name of the file to read host keys from
  76. :raises: ``IOError`` -- if there was an error reading the file
  77. """
  78. with open(filename, "r") as f:
  79. for lineno, line in enumerate(f, 1):
  80. line = line.strip()
  81. if (len(line) == 0) or (line[0] == "#"):
  82. continue
  83. try:
  84. e = HostKeyEntry.from_line(line, lineno)
  85. except SSHException:
  86. continue
  87. if e is not None:
  88. _hostnames = e.hostnames
  89. for h in _hostnames:
  90. if self.check(h, e.key):
  91. e.hostnames.remove(h)
  92. if len(e.hostnames):
  93. self._entries.append(e)
  94. def save(self, filename):
  95. """
  96. Save host keys into a file, in the format used by OpenSSH. The order
  97. of keys in the file will be preserved when possible (if these keys were
  98. loaded from a file originally). The single exception is that combined
  99. lines will be split into individual key lines, which is arguably a bug.
  100. :param str filename: name of the file to write
  101. :raises: ``IOError`` -- if there was an error writing the file
  102. .. versionadded:: 1.6.1
  103. """
  104. with open(filename, "w") as f:
  105. for e in self._entries:
  106. line = e.to_line()
  107. if line:
  108. f.write(line)
  109. def lookup(self, hostname):
  110. """
  111. Find a hostkey entry for a given hostname or IP. If no entry is found,
  112. ``None`` is returned. Otherwise a dictionary of keytype to key is
  113. returned. The keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``.
  114. :param str hostname: the hostname (or IP) to lookup
  115. :return: dict of `str` -> `.PKey` keys associated with this host
  116. (or ``None``)
  117. """
  118. class SubDict(MutableMapping):
  119. def __init__(self, hostname, entries, hostkeys):
  120. self._hostname = hostname
  121. self._entries = entries
  122. self._hostkeys = hostkeys
  123. def __iter__(self):
  124. for k in self.keys():
  125. yield k
  126. def __len__(self):
  127. return len(self.keys())
  128. def __delitem__(self, key):
  129. for e in list(self._entries):
  130. if e.key.get_name() == key:
  131. self._entries.remove(e)
  132. break
  133. else:
  134. raise KeyError(key)
  135. def __getitem__(self, key):
  136. for e in self._entries:
  137. if e.key.get_name() == key:
  138. return e.key
  139. raise KeyError(key)
  140. def __setitem__(self, key, val):
  141. for e in self._entries:
  142. if e.key is None:
  143. continue
  144. if e.key.get_name() == key:
  145. # replace
  146. e.key = val
  147. break
  148. else:
  149. # add a new one
  150. e = HostKeyEntry([hostname], val)
  151. self._entries.append(e)
  152. self._hostkeys._entries.append(e)
  153. def keys(self):
  154. return [
  155. e.key.get_name()
  156. for e in self._entries
  157. if e.key is not None
  158. ]
  159. entries = []
  160. for e in self._entries:
  161. if self._hostname_matches(hostname, e):
  162. entries.append(e)
  163. if len(entries) == 0:
  164. return None
  165. return SubDict(hostname, entries, self)
  166. def _hostname_matches(self, hostname, entry):
  167. """
  168. Tests whether ``hostname`` string matches given SubDict ``entry``.
  169. :returns bool:
  170. """
  171. for h in entry.hostnames:
  172. if (
  173. h == hostname
  174. or h.startswith("|1|")
  175. and not hostname.startswith("|1|")
  176. and constant_time_bytes_eq(self.hash_host(hostname, h), h)
  177. ):
  178. return True
  179. return False
  180. def check(self, hostname, key):
  181. """
  182. Return True if the given key is associated with the given hostname
  183. in this dictionary.
  184. :param str hostname: hostname (or IP) of the SSH server
  185. :param .PKey key: the key to check
  186. :return:
  187. ``True`` if the key is associated with the hostname; else ``False``
  188. """
  189. k = self.lookup(hostname)
  190. if k is None:
  191. return False
  192. host_key = k.get(key.get_name(), None)
  193. if host_key is None:
  194. return False
  195. return host_key.asbytes() == key.asbytes()
  196. def clear(self):
  197. """
  198. Remove all host keys from the dictionary.
  199. """
  200. self._entries = []
  201. def __iter__(self):
  202. for k in self.keys():
  203. yield k
  204. def __len__(self):
  205. return len(self.keys())
  206. def __getitem__(self, key):
  207. ret = self.lookup(key)
  208. if ret is None:
  209. raise KeyError(key)
  210. return ret
  211. def __delitem__(self, key):
  212. index = None
  213. for i, entry in enumerate(self._entries):
  214. if self._hostname_matches(key, entry):
  215. index = i
  216. break
  217. if index is None:
  218. raise KeyError(key)
  219. self._entries.pop(index)
  220. def __setitem__(self, hostname, entry):
  221. # don't use this please.
  222. if len(entry) == 0:
  223. self._entries.append(HostKeyEntry([hostname], None))
  224. return
  225. for key_type in entry.keys():
  226. found = False
  227. for e in self._entries:
  228. if (hostname in e.hostnames) and e.key.get_name() == key_type:
  229. # replace
  230. e.key = entry[key_type]
  231. found = True
  232. if not found:
  233. self._entries.append(HostKeyEntry([hostname], entry[key_type]))
  234. def keys(self):
  235. # Python 2.4 sets would be nice here.
  236. ret = []
  237. for e in self._entries:
  238. for h in e.hostnames:
  239. if h not in ret:
  240. ret.append(h)
  241. return ret
  242. def values(self):
  243. ret = []
  244. for k in self.keys():
  245. ret.append(self.lookup(k))
  246. return ret
  247. @staticmethod
  248. def hash_host(hostname, salt=None):
  249. """
  250. Return a "hashed" form of the hostname, as used by OpenSSH when storing
  251. hashed hostnames in the known_hosts file.
  252. :param str hostname: the hostname to hash
  253. :param str salt: optional salt to use when hashing
  254. (must be 20 bytes long)
  255. :return: the hashed hostname as a `str`
  256. """
  257. if salt is None:
  258. salt = os.urandom(sha1().digest_size)
  259. else:
  260. if salt.startswith("|1|"):
  261. salt = salt.split("|")[2]
  262. salt = decodebytes(b(salt))
  263. assert len(salt) == sha1().digest_size
  264. hmac = HMAC(salt, b(hostname), sha1).digest()
  265. hostkey = "|1|{}|{}".format(u(encodebytes(salt)), u(encodebytes(hmac)))
  266. return hostkey.replace("\n", "")
  267. class InvalidHostKey(Exception):
  268. def __init__(self, line, exc):
  269. self.line = line
  270. self.exc = exc
  271. self.args = (line, exc)
  272. class HostKeyEntry:
  273. """
  274. Representation of a line in an OpenSSH-style "known hosts" file.
  275. """
  276. def __init__(self, hostnames=None, key=None):
  277. self.valid = (hostnames is not None) and (key is not None)
  278. self.hostnames = hostnames
  279. self.key = key
  280. @classmethod
  281. def from_line(cls, line, lineno=None):
  282. """
  283. Parses the given line of text to find the names for the host,
  284. the type of key, and the key data. The line is expected to be in the
  285. format used by the OpenSSH known_hosts file.
  286. Lines are expected to not have leading or trailing whitespace.
  287. We don't bother to check for comments or empty lines. All of
  288. that should be taken care of before sending the line to us.
  289. :param str line: a line from an OpenSSH known_hosts file
  290. """
  291. log = get_logger("paramiko.hostkeys")
  292. fields = line.split(" ")
  293. if len(fields) < 3:
  294. # Bad number of fields
  295. msg = "Not enough fields found in known_hosts in line {} ({!r})"
  296. log.info(msg.format(lineno, line))
  297. return None
  298. fields = fields[:3]
  299. names, keytype, key = fields
  300. names = names.split(",")
  301. # Decide what kind of key we're looking at and create an object
  302. # to hold it accordingly.
  303. try:
  304. key = b(key)
  305. if keytype == "ssh-rsa":
  306. key = RSAKey(data=decodebytes(key))
  307. elif keytype == "ssh-dss":
  308. key = DSSKey(data=decodebytes(key))
  309. elif keytype in ECDSAKey.supported_key_format_identifiers():
  310. key = ECDSAKey(data=decodebytes(key), validate_point=False)
  311. elif keytype == "ssh-ed25519":
  312. key = Ed25519Key(data=decodebytes(key))
  313. else:
  314. log.info("Unable to handle key of type {}".format(keytype))
  315. return None
  316. except binascii.Error as e:
  317. raise InvalidHostKey(line, e)
  318. return cls(names, key)
  319. def to_line(self):
  320. """
  321. Returns a string in OpenSSH known_hosts file format, or None if
  322. the object is not in a valid state. A trailing newline is
  323. included.
  324. """
  325. if self.valid:
  326. return "{} {} {}\n".format(
  327. ",".join(self.hostnames),
  328. self.key.get_name(),
  329. self.key.get_base64(),
  330. )
  331. return None
  332. def __repr__(self):
  333. return "<HostKeyEntry {!r}: {!r}>".format(self.hostnames, self.key)