Осторожно: данный пост может вызывать непродолжительное обострение паранойи
Привет! Не верите ли вы в популярные продукты для защищённой переписки так, как не верю в них я? Например, в браузерные крипточаты с шифрованием на стороне клиента, или в p2p-криптомессенжеры?
В данном посте речь пойдет об организации защищённого общения между двумя собеседниками. Он адресован таким же недоверчивым людям как я, поэтому в нём не будет ни кода, написанного мной, ни изобретённых на коленке протоколов и алгоритмов. Будет использоваться только библиотека openssl и набор программ openssh.
Вступление
Во всех продуктах защищённой коммуникации, которые я встречал, у меня неизбежно возникал вопрос доверия их разработчикам. Я не понимал, почему, скажем, к автору продукта всё ещё не пришли и не попросили ослабить шифрование или отдавать клиенту с определённым ip-адресом особый javascript-код. Вы можете сказать: «это невозможно». Почитайте, например, недавний пост Почтовый сервис Lavabit вынужден закрыться, и, особенно, продолжение этой истории(англ.), прошедшее мимо хабра.
К сожалению, люди совершают ошибки. Взять, например, сервис, первый по ссылке гугла по запросам «secure chat» и «crypto chat», который называется Cryptocat. Примерно три месяца назад в его коде обнаружилась ошибка — из-за недостаточно хорошей реализации генератора случайных чисел групповые сообщения, которые передавались в период с 17 октября 2011 года по 15 июня 2013 года стало возможно расшифровать, см. Decryptocat. Причём, в начале 2013 года компанией Veracode проводился аудит кода, который присудил ей «Security Quality Score of 100/100», и это был не единственный аудит кода в этот период.
Поэтому возникла идея использовать для коммуникации библиотеки, открытые алгоритмы и протоколы с более чем десятилетней историей поиска уязвимостей. Для меня, как и, надеюсь, что для многих из вас — это алгоритмы и протоколы из openssl и openssh(который, кстати, использует openssl внутри), а именно: TLS, SSH, SHA1, AES256, RSA и D-H. Я буду рассказывать на их примере, но, конечно, вы можете заменить их на те, которым доверяете лично вы, основная идея при этом не изменится.
Об OpenSSL
OpenSSL — библиотека с открытым исходным кодом, реализующая протоколы SSL/TLS. Они используются каждый раз, когда вы заходите на сайт по HTTPS. Про это недавно была статья.
Вот список уязвимостей, которые нашли в OpenSSL за более чем 10 лет анализа.
Неполный список программ, которые используют OpenSSL: apache, nginx, squid, openvpn, openssh, ntp, dhcp, cups, syslog-ng, xorg-server, php, python, ruby, libevent, nodejs, curl, wget, links, lynx, socat, hostapd, wpa_supplicant, virtualbox, vmware-player, libreoffice, ffmpeg.
И совсем немного про TLS. TLS — это криптопротокол, который позволяет устанавливать защищённые соединения. Сейчас существуют 3 версии этого протокола: 1.0, 1.1 и 1.2. Они описаны, соответственно, в rfc 2246, rfc 4346 и rfc 5246. Забавно, что все они заканчиваются на 46. Мы будем использовать версию 1.0 т.к. она вышла в 1999 году и в ней не было найдено серьёзных уязвимостей.
Протокол очень хитрый. Очень упрощённо процесс выглядит так: стороны договариваются о ключе для симметричного шифрования и в дальнейшем его используют. Все сообщения защищаются от изменения/подмены при помощи сочетания хэш функции с секретным ключём(HMAC). Так же, в протоколе предусмотрена взаимная аутентификация сервера и клиента
Помимо полной реализации TLS, библиотека OpenSSL содержит множество криптографических и математических алгоритмов и имеет консольный интерфейс.
$ openssl prime 997
3E5 is prime
2) Генерация случайных чисел:
$ openssl rand -hex 16
2871002e6f3cff937d066da9d3017197
3) Вычисление контрольной суммы:
$ openssl sha1 /etc/passwd
SHA1(/etc/passwd)= 8b74a9f1ddf496c02bc85e0e120bbb903d922276
4) Шифрование:
$ openssl aes-256-cbc -k pass -in /etc/passwd
<много бинарного вывода>
Именно этот интерфейс мы и будем использовать для нашей задачи. Перейдём к делу.
Простой вариант
Первый собеседник (с «белым» ip, назовём его «сервер») выполняет:
openssl s_server -accept 4433 -nocert -cipher ADH-AES256-SHA -tls1 -no_ticket
Второй собеседник, клиент, выполняет:
openssl s_client -connect <host>:4433 -cipher ADH-AES256-SHA -tls1 -no_ticket
Что мы только что сделали? Первой командой мы запустили tls 1.0-сервер, а второй — подключились к нему. Теперь можно общаться, все сообщения шифруются алгоритмом AES256, ключ для шифрования согласуется с помощью алгоритма Диффи-Хеллмана(D-H). Вот отличное пятиминутное видео, объясняющее его суть. Алгоритм D-H позволяет получить общий секретный ключ, используя незащищённый от прослушивания канал связи.
На этом можно было бы и закончить, но у такого подхода есть три проблемы:
1) Он не защищает от атаки «человек посередине»(MITM, Man In The Middle). Идея атаки тривиальна — становимся между сервером и клиентом, клиент думает, что подключается к серверу, а на самом деле, подключается к нам(к злоумышленнику), а мы транслируем его трафик на настоящий сервер.
2) Требуется наличие белого ip хотя бы у одной стороны.
3) Даже если данные нельзя расшифровать, доступна «метаинформация» о соединении: кто с кем соединялся, когда и сколько данных передано.
Защищаемся от MITM
Для того, чтобы защититься MITM, нужно, чтобы сервер аутентифицировал клиента, а клиент — сервера. Это достигается использованием асимитричного шифрования.
На на сервере и на клиенте нужно сгенерировать закрытый RSA-ключ и публичный сертификат.
На сервере выполняем:
openssl genrsa -out server.key 2048
openssl req -new -key server.key -batch -days 3650 -x509 -out server.crt
На клиенте:
openssl genrsa -out client.key 2048
openssl req -new -key client.key -batch -days 3650 -x509 -out client.crt
Здесь, 2048 — количество бит в модуле закрытого ключа, 3650 — срок действия сертификата в днях.
Затем сервер должен передать свой сертификат(файл server.crt) клиенту, а клиент(файл client.crt) — серверу. Это слабое место, ведь злоумышленник может подменить сертификат в момент передачи(если он его просто прочитает, то ничего страшного). Поэтому, лучше передавать его, используя несколько каналов(pastebin, email, skype, jabber, sms, голосом по телефону, почтой России, стеганографией в фотографиях котят в блоге, и.т.д). Передать сертификат достаточно один раз.
После этого, команда для создания TLS-сервера будет выглядеть так:
openssl s_server -accept 4433 -cert server.crt -key server.key -Verify 0 -CAfile client.crt -cipher DHE-RSA-AES256-SHA -tls1 -no_ticket
Подключение к серверу:
openssl s_client -connect <host>:4433 -cert client.crt -key client.key -verify 0 -CAfile server.crt -cipher DHE-RSA-AES256-SHA -tls1 -no_ticket
Теперь сервер и клиент смогут аутентифицировать друг друга.
Важно: если аутентификация прошла не успешно, то ни сервер, ни клиент не разорвёт соединение! Они лишь напишут об ошибке. К сожалению, нет ключа, который бы менял это поведение, поэтому нужно внимательно смотреть в лог подключения. Ошибка выглядит примерно так:
verify error:num=18:self signed certificate
Будем считать, что проблему MITM мы решили, осталось понять как решить проблемы 2) и 3). Здесь нам поможет протокол SSH.
SSH спешит на помощь
Протокол SSH активно используется при администрировании UNIX-серверов. Он чем-то похож на TLS, передаваемые данные тоже шифруются и подписываются. О ключе для шифрования договариваются обе стороны, например, используя тот же алгоритм D-H.
Для того, чтобы осуществить наш коварный план по защищённому обмену информацией, нам понадобится сервер с доступом по SSH и с белым IP. Найти такой сервер в наши дни не составляет особой сложности. Лучше искать тот, к которому подключаются много и часто.
Одна из особенностей протокола — он позволяет создавать защищённые туннели. Этим мы и воспользуемся!
На сервере выполняем:
ssh -c aes256-cbc -m hmac-sha1-96 -o KexAlgorithms=diffie-hellman-group-exchange-sha1 -R 4433:127.0.0.1:4433 user@sshhost
nc -l -p 4433 -v
Здесь, для дополнительного спокойствия, мы явно указываем алгоритмы шифрования, обмена ключами и хэширования. Ключ -R значит, что все подключения на порт 4433 ssh-сервера пойдут в шифрованный ssh-туннель и попадут к ssh-клиенту на тот же самый порт. Если у пользователя установлен в качестве шелла /sbin/nologin, то иногда помогает использование ключа -N, который указывает ssh-клиенту, не исполнять никаких команд на сервере, а лишь только создать туннель.
На клиенте выполняем:
ssh -c aes256-cbc -m hmac-sha1-96 -o KexAlgorithms=diffie-hellman-group-exchange-sha1 -L 4433:127.0.0.1:4433 user@sshhost
nc 127.0.0.1 4433
Теперь, если клиент подключится к себе на 127.0.0.1 на порт 4433, то ssh-клиент запроксирует соединение по защищённому туннелю на порт 4433 удалённой машины, а оттуда, данные перешифруются и полетят по другому туннелю на машину-сервер.
Команда nc — это вызов популярной утилиты netcat. Она позволяет слушать и устанавливать tcp-соединения, а так же передавать по ним данные. При выполнении команд выше, стороннему наблюдателю будет казаться, что никакого туннеля нет и что это просто два ssh-подключения к одному и тому же серверу, никак не связанных между собой. Таким образом, получаем защищённое и относительно незаметное соединение.
Что плохо? То, что мы не можем доверять ssh-серверу! Теоретически он может писать расшифрованные данные к себе в лог в момент их перешифровки, когда они попадают из одного туннеля в другой.
SSH + OpenSSL или We have to go deeper
Теперь пустим наш TLS-трафик поверх цепочки SSH-туннелей. Если вы внимательно следили, то итоговая последовательность команд будет такой:
На сервере:
# следующие три строки обязательны только для соединения в первый раз, в дальнейшем - опциональны
openssl genrsa -out server.key 2048
openssl req -new -key server.key -batch -days 3650 -x509 -out server.crt
[передаём server.crt клиенту]
ssh -c aes256-cbc -m hmac-sha1-96 -o KexAlgorithms=diffie-hellman-group-exchange-sha1 -R 4433:127.0.0.1:4433 user@sshhost
openssl s_server -accept 4433 -cert server.crt -key server.key -Verify 0 -CAfile client.crt -cipher DHE-RSA-AES256-SHA -tls1 -no_ticket
[ждём соединения]
[внимательно смотрим на лог подключения на предмет ошибок валидации сертификата]
[общаемся]
На клиенте:
# следующие три строки обязательны только для соединения в первый раз, в дальнейшем - опциональны
openssl genrsa -out client.key 2048
openssl req -new -key client.key -batch -days 3650 -x509 -out client.crt
[передаём client.crt серверу]
ssh -c aes256-cbc -m hmac-sha1-96 -o KexAlgorithms=diffie-hellman-group-exchange-sha1 -L 4433:127.0.0.1:4433 user@sshhost
openssl s_client -connect localhost:4433 -cert client.crt -key client.key -verify 0 -CAfile server.crt -cipher DHE-RSA-AES256-SHA -tls1 -no_ticket
[внимательно смотрим на лог подключения на предмет ошибок валидации сертификата]
[общаемся]
Для того, чтобы ssh-клиент уходил в background после подключения, ему можно указать ключи -Nf.
Итог
Итак, не написав ни одной строчки кода и используя только стандартные инструменты, мы получили возможность переписываться, используя незаметное, способное работать за NAT'ом, соединение, защищённое от прослушивания, MITM-атак и подмены сообщений. К тому же, оно относительно просто для настройки — от каждого собеседника требуется выполнить всего по 4 команды и одну передачу файла для первого соединения, и по две команды для последующих. Да, ещё требуется доступ по SSH к любому серверу.
MiniFAQ
Q: Почему не VPN?
A: VPN — хорошее решение. Но он сложен в настройке, требует рутовых прав на сервере или доверия к серверу.
Q: Почему не GnuPG?
A: GnuPG — тоже хорошее решение. Но при желании переданные ранее данные можно расшифровать, получив закрытый ключ(например, вместе с ноутбуком).
Q: С чего вы взяли, что в OpenSSL нет уязвимостей?
A: Я уверен, что они там есть. OpenSSL лично мне субъективно кажется надёжным, поэтому я рассказывал на его примере. Можно использовать любую другую реализацию, которой доверяете лично вы.
Q: Обещаете ли вы, что команды правильные?
A: Не обещаю! Советую проверить!
Q: Как дела с кроссплатформенностью?
A: OpenSSL — кроссплатформенная. Клиенты SSH существуют под большинство ОС.
Q: Хочу веб-интерфейс, почему консоль?!
A: Для вэб-интерфейса требуется писать код. Одна небольшая XSS-уязвимость может стоить важных данных. Моя логика проста: нет кода — нет ошибок.
Q: Ошибка getaddrinfo: Name or service not known при выполнении команды openssl s_server
A: У вас, наверно, выключен IPv6. Обновите openssl.
Q: Ошибка gethostbyname failure при выполнении команды openssl s_server
A: Проверьте файл /etc/hosts. Первое имя для адреса 127.0.0.1 обязательно должно резолвиться.
Автор: alexbers