Перейти к содержимому

Закрепление идентификации сервера

Каждый сервер rVPN идентифицируется долговременным Ed25519 ключом идентификации. Клиент закрепляет этот ключ при первом подключении к профилю и отказывается соединяться, если закреплённый ключ когда-либо изменится без валидного доказательства ротации. Это та же модель доверия, что и в SSH, дополненная церемонией ротации на цепочке подписей — так оператор может менять ключи, не ломая уже настроенных клиентов.


Все отпечатки записаны в единой канонической кодировке:

ik:1:<52 символа base32>
  • ik: — пространство имён, зарезервировано под будущие типы ключей.
  • 1 — версия, чтобы легитимная ротация могла явно перейти к ik:2:….
  • Тело — sha256(identity_key) в base32 по RFC 4648, строчные буквы, без заполнителей. 52 символа, ASCII-безопасно, подходит для QR-кодов.

Пример:

ik:1:d4rgmp5b7ta6qmxi2mccwkjq4qxopfxzr7qivbfgu4wjycmuxnla

Раньше десктопный клиент хранил hex-отпечаток из 32 символов. Он всё ещё читает эти старые значения при первом подключении, а затем переписывает их в форме ik:1: — изменения конфигурации не требуются.


Как закрепление работает на мобильных устройствах

Заголовок раздела «Как закрепление работает на мобильных устройствах»

На iOS, macOS и Android отпечаток хранится в самом профиле, в поле Server Fingerprint. Отдельного known_hosts.json больше нет.

  • Первое подключение при пустом Server Fingerprint и включённом Trust on First Use: приложение фиксирует отпечаток сервера и сохраняет его в профиле.
  • Последующие подключения: туннель открывается только если отпечаток сервера совпадает с сохранённым значением.
  • Несовпадение: приложение показывает диалог Server identity changed с закреплённым и полученным отпечатками рядом и тремя вариантами — Cancel, Delete Profile, Trust New Identity.

Чтобы закрепить идентификацию явно до первого подключения, вставьте отпечаток (полученный от оператора в форме ik:1:) в поле Server Fingerprint редактора профиля.


Десктопный клиент rvpn хранит отпечатки в known_hosts.json (путь настраивается) и накладывает поверх этого отпечаток из конфига. Полное описание полей см. в разделе [server_identity] на странице Конфигурация клиента; кратко:

  • fingerprint — задайте значение ik:1:…, чтобы принудительно использовать конкретный ключ, независимо от содержимого known_hosts.json.
  • trust_on_first_use — по умолчанию true. Захватывать при первом подключении.
  • strict_mode — по умолчанию false. Установите true, чтобы полностью отказывать неизвестным серверам; в этом случае fingerprint нужно задать до первого подключения.
  • strict — по умолчанию true. При false несовпадение приводит только к предупреждению в логах, а соединение не разрывается.

При сохранении known_hosts.json клиент также мигрирует любой найденный устаревший hex-отпечаток в канонический ik:1:, сохраняя исходную метку first_seen.


Когда оператор меняет ключ идентификации сервера, prekey bundle получает два новых поля, по которым клиент проверяет непрерывность цепочки:

  • identity_key_version — монотонное; новая идентификация ⇒ +1.
  • rotation_signature — Ed25519-подпись предыдущим ключом идентификации над байтами нового публичного ключа плюс номером новой версии.

Клиент, закрепивший версию N, сделает следующее:

  1. Увидит bundle, чей ключ не совпадает с сохранённым отпечатком,
  2. Проверит, что identity_key_version == N + 1,
  3. Проверит подпись ротации, используя закреплённую идентификацию как ключ верификации,
  4. Если обе проверки пройдены — молча обновит сохранённый отпечаток на новое значение, без диалога, соединение просто работает.

Если клиент видит неподписанную ротацию или пропуск версии, он падает обратно к диалогу несовпадения, описанному выше.

Свежие серверы публикуют v1 bundle без подписи ротации:

Окно терминала
rvpn-server keygen
rvpn-server prekey-bundle

Для ротации сгенерируйте новый ключ идентификации и подпишите новый bundle старым ключом:

Окно терминала
# Сохраняем старый ключ, чтобы подписать ротацию
mv server_identity.key old_identity.key
# Генерируем новый
rvpn-server keygen --output server_identity.key
# Публикуем v2 bundle, подписанный старым ключом
rvpn-server prekey-bundle \
--identity server_identity.key \
--output prekey-bundle.json \
--rotate-from old_identity.key \
--from-version 1

--rotate-from и --from-version должны использоваться вместе. Сервер отказывается публиковать неподписанную ротацию. Выложите новый bundle; уже закреплённые клиенты обновятся молча, новые — закрепят новый ключ при первом подключении.

Заархивируйте old_identity.key — он нужен ровно до момента публикации новой ротации, но, уничтожив его, вы навсегда лишаетесь возможности подписать следующую ротацию с этой версии.


И мобильный, и десктопный экспорт профиля переносят поле serverFingerprint. Когда вы импортируете .rvpn профиль с уже установленным отпечатком, отправитель поручился за идентификацию сервера внесистемно — сравните опубликованный им отпечаток с тем, что показан на экране импорта, прежде чем сохранять.

Если у импортируемого профиля отпечатка нет, туннель захватит его при первом успешном подключении — так же, как если бы вы создали профиль локально.


Защищает от

  • Пассивного MITM после первого успешного подключения.
  • Ситуации, когда враждебный оператор перехватывает домен или IP вашего сервера и направляет их на другой бэкенд.
  • Повтора украденного prekey bundle с другого сервера.

Не защищает от

  • Активного MITM при самом первом подключении. Классическое ограничение TOFU. Смягчается импортом профиля с уже вложенным отпечатком или его вставкой из внесистемного канала до подключения.
  • Модификации локального хранилища профиля на устройствах с root или jailbreak. Штатная iOS изолирует профили в песочнице; штатный Android держит их как приватные для приложения.

Если вам нужно намеренно заменить идентификацию сервера — например, после компрометации — правильный путь именно через диалог несовпадения. Опубликуйте новый отпечаток внесистемно, попросите пользователей принять изменение, и они вернутся к новому ключу со свежим отпечатком.