Каждый раз, когда мы подключаемся по протоколу ssh к серверу, ssh клиент проверяет совпадает ли публичный ключ для этого сервера с тем, который был прошлый раз (по крайней мере так рекомендует делать стандарт ssh). В OpenSSH список известных ключей серверов хранится в файле known_hosts. Под катом коротко о том, что и как конкретно там хранится.

Все эксперименты проводились на Linux (Debian/Mint/Ubuntu). За расположение и содержание файлов в других ОС не ручаюсь.

Подключаясь первый раз к ssh серверу, мы видим примерно такое сообщение:
The authenticity of host '192.168.0.2 (192.168.0.2)' can't be established.
RSA key fingerprint is SHA256:kd9mRkEGLo+RBBNpxKp7mInocF3/Yl/0fXRsGJ2JfYg.
Are you sure you want to continue connecting (yes/no)?
Если согласиться, то в файл ~/.ssh/known_hosts добавится такая строка:
|1|CuXixZ+EWfgz40wpkMugPHPalyk=|KNoVhur7z5NAZmNndtwWq0kN1SQ= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCeiF4OOOUhWvOYrh/e4q91+iz+i9S0s3M2LPq+GAhRlhKt5vKyEVd6x6m26cc98Y+SQXnCB9GWeVYk8jlFHEXnY4YWeWLDwXIhHBJYt5yz3j5Wkg95x+mPvO9FLSBk/Al2GbH5q6F+hZIlLmO6ciISmX4TtcG1sw4SwoTADrrhdM0OJd+c5CU8iqCbc6PznYbLZXCvqPZTWeSbTLUcUu1Ti+7xGwT8DF+tIyLFcU+zxd0QnwJIbNvewkHs0LsMOWFVPz/Nd0XiVXimX+ugCDBZ/4q8NUwH9SGzCMAvnnr+D1I8X2vhSuRsTsQXL5P3vf8elDxPdDrMJzNtlBCbLWzV
Тут через пробел записаны три элемента: хэш от имени сервера, название используемого ассиметричного алгоритма и публичный ключ сервера. Разберём их по очереди.

А если почитать инструкцию
На самом деле согласно мануалу к Убунте там могут быть ещё 2 поля, так же отделённые пробелами:
  • в начале строки может находиться пометка "@cert-authority" или “@revoked”, означающие, соответственно, что в этой строке записан публичный ключ ЦА или что этот ключ был отозван и не может быть использован.
  • в конце строки может быть произвольный комментарий


Имя сервера


В примере хэш от имени сервера (хоста) выглядит так:
|1|CuXixZ+EWfgz40wpkMugPHPalyk=|KNoVhur7z5NAZmNndtwWq0kN1SQ=
На самом деле тут может быть записано и имя хоста в открытом виде или маска, задающая множество допустимых имён. Но у меня по умолчанию сохраняется хэшированое имя. Запись разделена на 3 части символом "|". Первая часть — алгоритм хэширования. «1» соответствует HMAC-SHA1 (других не видел). Вторая часть — соль (ключ для HMAC). Третья часть — собственно хэш (вывод HMAC).

Проверяем
from base64 import b64decode
import hmac

salt = b64decode("CuXixZ+EWfgz40wpkMugPHPalyk=")
host = b'192.168.0.2'
hash = hmac.HMAC(salt, host, 'sha1').digest()
print(b64encode(hash).decode())

> 'KNoVhur7z5NAZmNndtwWq0kN1SQ='

Ассиметричный алгоритм


В RFC-4253 перечислены 4 ассиметричных алгоритма: ssh-dss (по стандарту обязательный, но считается слабым и начиная с OpenSSH7.0 выключен по-умолчанию), ssh-rsa (рекомендуемый), pgp-sign-rsa (опциональный), pgp-sign-dss (опциональный). По умолчанию в Linux генерируются ключи первых двух видов и для не упомянутых в RFC алгоритмов на эллиптических кривых. Предпочтение отдаётся последним, однако клиент может выбрать алгоритм опцией HostKeyAlgorithms.

Как проверить нужный (не по-умолчанию) отпечаток ключа
Это может быть полезно если, например, при первом заходе на сервер вы хотите проверить отпечаток ключа, а знаете только отпечаток ключа ssh-rsa. Тогда можно подключиться такой командой:
ssh root@192.168.0.2 -o HostKeyAlgorithms=ssh-rsa

Если нужно задать ещё и алгоритм хэширования ключа, то можно использовать опцию FingerprintHash. Например, если известен только md5 от ssh-rsa можно подключиться так:
ssh root@192.168.0.2 -o HostKeyAlgorithms=ssh-rsa -o FingerprintHash=md5


Публичный ключ


Публичный ключ в known_hosts совпадает с тем, который записан в файле /etc/ssh/ssh_host_rsa_key.pub на сервере (вместо rsa подставить название используемого алгоритма). Если снять Base64 кодирование, то внутри будет ещё раз название алгоритма и собственно компоненты ключа.

А чего бы и не снять Base64
b'\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x01\x00\x9e\x88^\x0e8\xe5!Z\xf3\x98\xae\x1f\xde\xe2\xafu\xfa,\xfe\x8b\xd4\xb4\xb3s6,\xfa\xbe\x18\x08Q\x96\x12\xad\xe6\xf2\xb2\x11Wz\xc7\xa9\xb6\xe9\xc7=\xf1\x8f\x92Ay\xc2\x07\xd1\x96yV$\xf29E\x1cE\xe7c\x86\x16yb\xc3\xc1r!\x1c\x12X\xb7\x9c\xb3\xde>V\x92\x0fy\xc7\xe9\x8f\xbc\xefE- d\xfc\tv\x19\xb1\xf9\xab\xa1~\x85\x92%.c\xbar"\x12\x99~\x13\xb5\xc1\xb5\xb3\x0e\x12\xc2\x84\xc0\x0e\xba\xe1t\xcd\x0e%\xdf\x9c\xe4%<\x8a\xa0\x9bs\xa3\xf3\x9d\x86\xcbep\xaf\xa8\xf6SY\xe4\x9bL\xb5\x1cR\xedS\x8b\xee\xf1\x1b\x04\xfc\x0c_\xad#"\xc5qO\xb3\xc5\xdd\x10\x9f\x02Hl\xdb\xde\xc2A\xec\xd0\xbb\x0c9aU??\xcdwE\xe2Ux\xa6_\xeb\xa0\x080Y\xff\x8a\xbc5L\x07\xf5!\xb3\x08\xc0/\x9ez\xfe\x0fR<_k\xe1J\xe4lN\xc4\x17/\x93\xf7\xbd\xff\x1e\x94<Ot:\xcc\'3m\x94\x10\x9b-l\xd5'
Видно, что идут 4 байта, в которые записана длина поля, потом само поле и т.д. Первое поле — название алгоритма, остальные зависят от конкретного алгоритма. В приведённом выше ключе 3 поля:
b'ssh-rsa' - название 
b'\x01\x00\x01' - публичная экспонента
b'\x00\x9e\x88^\x0e8\xe5!Z\xf3\x98\xae\x1f\xde\xe2\xafu\xfa,\xfe\x8b\xd4\xb4\xb3s6,\xfa\xbe\x18\x08Q\x96\x12\xad\xe6\xf2\xb2\x11Wz\xc7\xa9\xb6\xe9\xc7=\xf1\x8f\x92Ay\xc2\x07\xd1\x96yV$\xf29E\x1cE\xe7c\x86\x16yb\xc3\xc1r!\x1c\x12X\xb7\x9c\xb3\xde>V\x92\x0fy\xc7\xe9\x8f\xbc\xefE- d\xfc\tv\x19\xb1\xf9\xab\xa1~\x85\x92%.c\xbar"\x12\x99~\x13\xb5\xc1\xb5\xb3\x0e\x12\xc2\x84\xc0\x0e\xba\xe1t\xcd\x0e%\xdf\x9c\xe4%<\x8a\xa0\x9bs\xa3\xf3\x9d\x86\xcbep\xaf\xa8\xf6SY\xe4\x9bL\xb5\x1cR\xedS\x8b\xee\xf1\x1b\x04\xfc\x0c_\xad#"\xc5qO\xb3\xc5\xdd\x10\x9f\x02Hl\xdb\xde\xc2A\xec\xd0\xbb\x0c9aU??\xcdwE\xe2Ux\xa6_\xeb\xa0\x080Y\xff\x8a\xbc5L\x07\xf5!\xb3\x08\xc0/\x9ez\xfe\x0fR<_k\xe1J\xe4lN\xc4\x17/\x93\xf7\xbd\xff\x1e\x94<Ot:\xcc\'3m\x94\x10\x9b-l\xd5' - модуль N (0x101 * 8 = 2048 бит)


Отпечаток ключа (Fingerprint)


Отпечаток ключа, который предлагается сверить при первом подключении — это соответствующий хэш (в примере — SHA256) от публичного ключа из прошлого пункта и из /etc/ssh/ssh_host_rsa_key.pub, закодированный в base64 для хэш функций семейства SHA или в hex для MD5.

Считаем
from hashlib import sha256
from base64 import b64decode, b64encode
pub_key_bin = b64decode("AAAAB3NzaC1yc2EAAAADAQABAAABAQCeiF4OOOUhWvOYrh/e4q91+iz+i9S0s3M2LPq+GAhRlhKt5vKyEVd6x6m26cc98Y+SQXnCB9GWeVYk8jlFHEXnY4YWeWLDwXIhHBJYt5yz3j5Wkg95x+mPvO9FLSBk/Al2GbH5q6F+hZIlLmO6ciISmX4TtcG1sw4SwoTADrrhdM0OJd+c5CU8iqCbc6PznYbLZXCvqPZTWeSbTLUcUu1Ti+7xGwT8DF+tIyLFcU+zxd0QnwJIbNvewkHs0LsMOWFVPz/Nd0XiVXimX+ugCDBZ/4q8NUwH9SGzCMAvnnr+D1I8X2vhSuRsTsQXL5P3vf8elDxPdDrMJzNtlBCbLWzV")
hash = sha256(pub_key_bin).digest()
fingerprint = b64encode(hash)
print(fingerprint)

> b'kd9mRkEGLo+RBBNpxKp7mInocF3/Yl/0fXRsGJ2JfYg='

Видим, что хэш и правда совпадает с отпечатком, показанным при первом подключении (цитата в начале статьи), с точностью до символа "=" в конце.

Тут небольшая программка для поиска хостов в файле known_hosts, появившаяся в процессе экспериментов.

Комментарии (16)


  1. xcore78
    29.08.2018 00:46
    +2

    DSA (ssh-dss) к использованию не рекомендуется, его поддержка уже выключена по-умолчанию в openssh 7 и выше.

    www.openssh.com/legacy.html


    1. Nokta_strigo Автор
      29.08.2018 01:29

      Да, OpenSSH7.2 у меня по-умолчанию ssh_host_dsa_key сгенерировал. Но подключения с ним не принимает.


  1. Daniyar94
    29.08.2018 00:56

    Не понимаю смысл? Потенциальный злоумышленник не может просто постучатся к машине, словить ее публичный ключ, и потом просто предоставить его жертве (скажем скомпрометировал DNS сервера у жертвы и перенаправляет его на свою машину)?


    1. Nokta_strigo Автор
      29.08.2018 01:17

      Во время обмена ключами сервер подписывает своим приватным ключём свою посылку (она каждый раз разная, к тому же включает число, полученное от клиента — оно тоже каждый раз разное и генерируется клиентом). Если у злоумышленника нет приватного ключа, то он не сможет её подписать и клиент прекратит установление соединения. Повторно передать записанную посылку он тоже не может, т.к. клиент увидит, что она не соответствует тому, что он только что послал.


      1. Dmitri-D
        29.08.2018 03:04

        Если у злоумышленника нет приватного ключа, то он не сможет её подписать и клиент прекратит установление соединени

        не совсем так. Если у злоумышленика нет приватного ключа _вашего_ сервера то он использует свой. Поэтому его public ключ и, соответственно, fingerprint тоже отличается, от того что клиент запомнил в своем локальном known_hosts файле. Обнаружив изменения, клиент _не_ разорвет соединение. Он сообщит пользователю об изменении идентификации сервера — ровно то сообщение, что в шапке статьи, и если клиент проигнорирует его, т.е. согласится с изменениями, то MITM вполне так себе перехватит сессию и будет видеть весь трафик в исходном виде


        1. andreymal
          29.08.2018 03:11

          Он сообщит пользователю об изменении идентификации сервера

          … и разорвёт соединение :) Чтобы MitM случился, пользователь должен не «согласиться с изменениями» (ничего подобного в этом случае нет), а ручками вычистить несовпадающий ключ из known_hosts и подключиться заново


          1. Dmitri-D
            29.08.2018 03:21

            зависит от опции
            -o StrictHostKeyChecking=no или yes


            1. andreymal
              29.08.2018 03:27

              Нет, разорвёт, смотрите сами:

              Заголовок спойлера


              1. Dmitri-D
                29.08.2018 03:31

                разумеется отредактировал, имею право 5 минут )).
                Я просто привык к этой опции в no и ниже написал почему она no


              1. celebrate
                29.08.2018 11:52

                StrictHostKeyChecking=no используется например в облачных CI/CD средах, где виртуалки в определенной подсети многократно создаются и убиваются. Там это вполне оправданно.


          1. Dmitri-D
            29.08.2018 03:30

            кстати, если ключ на сервере поменялся, а вы должны периодически его менять в полном соответствии с предписаниями и рекомендациями ssh, или если обновась версия и добавились новые HMAC методы (и заодно задепрекейтились старые), то fingerprint поменяется и это будет false positive. Вам нужно будет на всех клиентских хостах ручками чистить. Хорошо если вы один сам себе режиссер. А если это компания вроде нашей, где 1500 человек разработчиков и десятки тысяч серверов то, каждое обновление серверных ключей или HMAC — и полный геморрой с чисткой known_hosts. Неудобное решение. Централизованно подписанные сертификаты были бы удобнее.


            1. Nokta_strigo Автор
              29.08.2018 10:22

              В живую централизовано подписанных сертификатов в SSH не видел, но мануале к Убунте пишут про метку "@cert-authority" как раз в known_hosts, указывающую что эта строка описывает доверенный CA.
              В RFC 4251 пишут что CA тоже могут поддерживаться. Ещё есть RFC 6187 X.509v3 Certificates for Secure Shell Authentication. Ну и упомянутые в статье алгоритмы, основанные на PGP тоже должны давать возможность централизации доверия.


              1. Nokta_strigo Автор
                29.08.2018 10:36

                Мануал к Ubuntu про генерацию сертификатов для SSH (не X.509).


  1. saipr
    29.08.2018 08:13

    и добавились новые HMAC методы

    Вот они новые HMAC от ГОСТ


  1. nvv
    29.08.2018 17:49

    Это супер краткий пересказ документации с уточнением, что видел так в одной системе?


    1. g0rd1as
      30.08.2018 22:41

      Я когда увидел заголовок, как-то даже слегка пригорел… :) Типа: «Чтоа?! Никто разве этот файл не читал?!» Провокация, однако. :-D