Статья Змея в пакете. Создаем TLS соединение на Python

Logotype.jpg

Введение
С появлением интернет технологий жизнь людей значительно упростилась. Все покупки, работу и другие дела можно выполнить не вставая из-за компьютера. Но вместе с этим пришли и те, кто хочет заполучить чужую конфиденциальную информацию. Чаще всего таких людей принято называть злоумышленниками. Самый простой пример их работы - это перехват трафика в публичном месте, к примеру кафе. Огромное количество людей, а значит много информации, которую можно вытащить, используя пару утилит. Но сегодня речь пойдет немного о другом. Я расскажу тебе, как защитить свою информацию при передаче, и как происходит сам процесс соединения между двумя устройствами. Обо всем по порядку.

План работы

Перед тем как перейти к работе, следует набросать план, по которому мы будем работать. Для начала я расскажу немного теории. Поговорим о работе TLS, затронем тему передачи пакетов и немного приоткроем завесу на протокол Диффи-Хеллмана. Заранее скажу, что для адекватного восприятия информации тебе потребуется ознакомится с основами передачи данных. О них я говорил в одной из своих статей. Поэтому в случаи проблем со знаниями ты знаешь куда обращаться. В практической же части статьи мы создадим TLS соединение при помощи языка Python версии 3.10, в нем будут участвовать клиент и сервер. Чтобы все корректно работало тебе потребуется любой дистрибутив Linux с утилитой openssl. Приступим к работе.

Что такое транспортный протокол

Перед тем как объяснить тебе что это такое вспомни о видах шифрования. В основном их делят на два типа: симметричное шифрование и ассиметричное. Разница лишь в генерации ключа. В первом случаи для создания надежного соединения вся информация передается напрямую. Ассиметричное же шифрование предполагает, что все требуемые данные для соединения будут генерироваться на устройствах с минимальным обменом информации.

Допустим ты имеешь публичный ключ, который состоит из 256 символов. Чтобы установить соединение со своим другом тебе следует передать ему этот ключ и получить от него точно такой же. Это как своеобразное приветствие на языке компьютеров. После этого стоит подумать о безопасности сделанного соединения и дополнительно создать приватный ключ, который никто не сможет разгадать. И здесь возникает вопрос о том, как доставить этот ключ, ведь он будет передаваться в открытом доступе и его сможет прочитать любой человек. В этом случаи обеим сторонам стоит договориться о передачи.

Для ассиметричного шифрование такое не свойственно, ведь для отправки сообщения используется совокупность открытого и закрытого ключа. И на основе этих методов мы получаем протокол защиты транспортного уровня, он же TLS (Transport Layer Security). Для установки соединения требуется обменяться двумя сообщениями, которые содержат nonce-число и публичный ключ. Чтобы тебе было понятнее я наглядно изобразил схему обмена информацией. Ее ты можешь увидеть ниже.


TLS-connect.jpg


При помощи таких простых обменов между сервером и клиентом устанавливается надежное соединение. После получения ключей и чисел оба устройства генерируют приватный ключ, на основе которого в дальнейшем будут передаваться данные. Также у такого типа передачи информации есть несколько небольших нюансов, о которых мы сейчас поговорим.

Чтобы не загружать твой мозг я коротко расскажу про то, что такое хеш-функция и зачем она нужна в нашей ситуации. Представь, что ты отправляешь посылку и хочешь убедиться в том, что она дойдет до получателя в целостности. На какие показатели ты можешь рассчитывать? Если это книга можно взять ее толщину или состояние влажности. В случаи с какой-то объемной посылкой можно рассчитывать на ее массу. Если прикрепить к посылке бирку с этим показателем, то на таможне люди смогут узнать повреждена посылка или нет. Так и здесь. Хеш-функция не дает злоумышленнику отправлять измененные данные в зашифрованном виде. Он может перехватить пакет и видоизменить его, но когда он дойдет до компьютера получателя, то просто будет отклонен. Хеш-функция создает одну и ту же строку фиксированной длины при использовании одних и тех же входных данных.

Также по мимо перехвата пакета не будем забывать о существовании ARP-спуфинга. Ведь в этом виде атак злоумышленник выступает как сервер и клиент одновременно. Поэтому чтобы удостовериться в том, что мы отправляем сообщение нужному человеку следует использовать центры сертификатов и подписи. Как это работает? Перед обменом данными сервер предоставляет тебе свой сертификат, документ в электронном виде, который подтверждает владельца предоставленного ключа. Работает все это дело при помощи алгоритма проверки подписи.


Центры сертификации и подписи

Теперь давай разберем, как формируется подпись. Для ее создания можно использовать алгоритм RSA. Предположим, нам требуется подписать сообщение, чтобы это сделать требуется первым делом вычислить хеш с помощью функции SHA-256, а после зашифровать все это дело закрытым ключом. В результате огромный текст из символов и будет вашей подписью. Чтобы убедиться в этом нужно все расшифровать и сравнить подпись с хэшем. Если все совпадает, то дальнейшая передача информации не прервется.

Как ты понимаешь, сертификаты тоже должны быть подтверждены. Поэтому на этот случай существует центр сертификации, который подписывает все это дело доверенной инфраструктурой открытых ключей (public key infrastructure, PKI). Давай я более подробно расскажу, что это за термин.

PKI — это набор защищенных серверов, которые подписывают и хранят заверенные копии сертификатов. Например ты должен платить за регистрацию своего сертификата в промежуточном центре сертификации (intermediate certificate authority, ICA), чтобы твой друг смог проверить сертификат во время TLS-рукопожатия.

Большинство браузеров уже автоматически настроены на использование открытого ключа ICA, поэтому они доверяют сообщениям, которые подписаны через него. Описывать процесс проверки сертификата я не стану, более подробную информацию ты сможешь найти при правильном запросе. Также бывают ситуации, когда браузеры получают сертификат ICA с неизвестным для них открытым ключом. В этих случаях браузер должен проверить его с помощью других открытых ключей.

В основном это все, что тебе следует знать о работе сертификатов, подписей и их центров. Но давай вернемся в самое начало статьи и вспомним про наше синхронное шифрование. Ведь перед передачей получатель и отправитель должны вычислить общий ключ. И для решения такой задачи на помощь приходит алгоритм Диффи-Хеллмана.

Алгоритм Диффи-Хеллмана

Итак, напомню, что для установки соединения нам требуется вычислить общий ключ. Сам алгоритм нахождения заключается в математических решениях. Их я постараюсь задействовать как можно меньше и попробую простым языком объяснить как это все работает. Первым делом разобьем всю работу на несколько этапов.
  1. Генерация общих параметров;
  2. Создание открытого и закрытого ключа;
  3. Обмен открытыми ключами и nonce-числами;
  4. Вычисление общего секретного ключа;
  5. Формирование ключа.
На первом этапе создаются общие параметры для вычисления открытого и закрытого ключа. На примере возьмем две переменные J и Y. Первая переменная будет являться генератором чисел, вторая же используется для ограничения возможных значений. Это упрощает математические расчеты без влияния на их обоснованность.

Второй этап представляет из себя выбор чисел. Получатель и отправитель определяют случайным образом номера, которые будут служить им в качестве закрытого ключа. Далее получатель и отправитель смогут вычислить открытые ключи по формулам. Если говорить простым языком, то на этом этапе идет создания всего необходимого для надежного соединения. Обычно ключи имеют размер 2048 бит, но то же АНБ для безопасности советует применять их с размером в 3072 бита. Чтобы понимать насколько это затратно представь, что один компьютер за семь часов работы способен сгенерировать 6144-битный ключ. Чтобы сгенерировать все это дело воспользуемся командой:

Bash:
openssl pkeyparam -in parametersPG.pem -text
openssl genpkey -paramfile parametersPG.pem -out PPKey.pem

Ну а чтобы оценить результат следует открыть еще одно окно консоли и напечатать в нем такие строки:

Bash:
openssl pkey -in PPKey.pem -text -noout

Как ты заметил мы сгенерировали не только ключ, но и добавили к нему параметр. За это отвечает первая команда.

Перейдем к самому интересному. На этапе создания общего секретного ключа происходит следующий алгоритм. Предположим, что получатель у нас P, а отправитель это O. Тогда, чтобы получить общий секретный ключ мы должны возвести открытый ключ P в секретный ключ O. Таким образом мы получаем общие данные, которые сгенерированы без передачи какой-либо лишней информации. Если сделать все на формулах, то получится следующие (pk - открытый ключ, sk - секретный ключ, opk - общий ключ):

Код:
OPK(O) = PK(P)^SK(O)
OPK(P) = PK(O)^SK(P)

На практике чтобы получить один ключ на основе другого тебе требуется два файла с названием key и окно терминала под рукой. Открываем его и пишем такой код:

Bash:
openssl pkeyutl -derive -inkey first_key.pem -peerkey second_key.pem -out name_output_1.bin

Теперь повторяем всю ту же процедуру, но слегка в другом порядке:

Bash:
openssl pkeyutl -derive -inkey second_key.pem -peerkey first_key.pem -out name_output_2.bin

Чтобы насладиться результатом следует воспользоваться утилитой xxd. Думаю как с ней работать ты знаешь, поэтому останавливаться на этом я не стану. Но если ключом мы можем только любоваться, то как понять, что мы все сделали верно? Для этого используем команду cmp и указываем следом два файла. В результате программа сравнит их содержимое и скажет свой вердикт по этому поводу. Ну а если экран полностью пуст, то ключи идентичны друг другу.

Финальный этап заключается в том, чтобы сформировать готовые ключи. Здесь не придется учить новые формулы или открывать для себя, что то новое. Все действие производится при помощи одной команды. Будем использовать три файла для примера шифрования.

Bash:
openssl enc -aes-256-ctr -hkdf -e -a -in rock.txt -out encrypted.txt -pass file:name_output_1.bin

В результате документ rock.txt будет зашифрован с использованием ключа name_output_1.bin, а имя итогового файла будет encrypted.txt. Готово! Теперь ты знаешь как и зачем нужен Диффи-Хеллман в TLS протоколе. На основе полученных знаний я расскажу тебе как самим создать такое соединение. Также, чтобы лучше запомнить информацию я оставил ниже визуализацию алгоритма Диффи-Хеллмана. Ну а сейчас открывай свою любимую IDE и не откладывай далеко окно консоли, ведь в скором времени мы к нему вернемся.


Диффи-Хеллман.png


TLS-соединение на Python

Первым делом я опишу тебе создание клиентской части нашего соединения. Для его работы потребуется библиотека socket и ssl. Добавляем их в наш код и следом записываем переменные для хранения ключей и сертификатов. Также не забываем про хост, куда все будет отправляться. Имя файлов можешь использовать абсолютно любые, в моем случаи скрипты будут называться secureClient и secureServer.

Python:
import socket
import ssl

client_key = 'client.key'
client_cert = 'client.crt'
server_cert = 'server.crt'
port = 1234
hostname = '127.0.0.1'

После этого нужно задать SSL-контекст. Если говорить коротко, то это класс для управления сертификатами и другими параметрами сокетов.

Python:
context = ssl.SSLContext(ssl.PROTOCOL_TLS, cafile=server_cert)
context.load_cert_chain(certfile=client_cert, keyfile=client_key)
context.load_verify_locations(cafile=server_cert)
context.verify_mode = ssl.CERT_REQUIRED
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

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

Python:
with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_side=False, server_hostname=hostname) as socks:
        print(socks.version())
        message = input("Please enter your message: ")
        socks.send(message.encode())
        receives = socks.recv(1024)
        print(receives)

На этом код клиентской части готов, но ты скорее всего задаешься вопросом, откуда взять все файлы? Для этого воспользуемся любимой утилитой openssl и сформируем всю необходимую информацию при помощи команды:

Bash:
openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout client.key -out client.crt

Теперь, чтобы ты не запутался в большом количестве флагов ниже я расписал, где и что формируется.
  • -newkey: создает новый ключ с шифрованием RSA и длинной в 3072 бита;
  • -days: ставит срок годности сертификата в нашем случаи это 365 дней;
  • -nodes: дает openssl понять, что нужно сгенерировать незашифрованный закрытый ключ;
  • -x509: задает выходной формат сертификата;
  • -keyout: задает имя выходного файла.
После этого у тебя появится вся необходимая информация для программы. Повторим то же действия, но сменив название client на server. Также одно из главных преимуществ алгоритма Диффи-Хеллмана, которое мы сейчас используем, заключается в том, что при перехвате закрытого ключа злоумышленник сможет прочитать только прошлые сообщения. По другому это называется прямой секретностью.

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

Python:
import socket
import ssl

client_cert = 'client.crt'
server_key = 'server.key'
server_cert = 'server.crt'
port = 1234

Также создаем нашу оболочку и задаем файлы отвечающие за сертификат и ключ:

Python:
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.verify_mode = ssl.CERT_REQUIRED
context.verify_mode = ssl.CERT_REQUIRED
context.load_cert_chain(certfile=server_cert, keyfile=server_key)
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

Далее по прежнему открываем соединение и вместо приглашения на отправку сообщений создаем код отвечающий за принятия всех входящих пакетов.

Python:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
    sock.bind(('', port))
    sock.listen(1)
    with context.wrap_socket(sock, server_side=True) as socks:
        conn, addr = socks.accept()
        print(addr)
        message = conn.recv(1024).decode()
        capitalizedMessage = message.upper()
        conn.send(capitalizedMessage.encode())

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

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

Подводим итоги

Я думаю, что начало нашей работы ты уже не вспомнишь из-за сильной нагрузки на мозг. К счастью я напомню тебе нашу первоначальную цель, а именно создание безопасного соединения на Python. К сожалению даже протоколы Диффи-Хеллмана не безопасны. Для их обхода потребуется огромная лаборатория серверов и метод перебора всех ключей с заранее продуманной стратегией. С другой стороны на такое безумие пошли только АНБ, а это значит, что в теории протокол до сих пор является неуязвимым для атак со стороны злоумышленников.

Python:
import socket
import ssl

# Генерация ключа сервера
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout server.key -out server.crt

# Генерация ключа клиента
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout client.key -out client.crt

client_key = 'client.key'
client_cert = 'client.crt'
server_cert = 'server.crt'
port = 1234
hostname = '127.0.0.1'

context = ssl.SSLContext(ssl.PROTOCOL_TLS, cafile=server_cert)
context.load_cert_chain(certfile=client_cert, keyfile=client_key)
context.load_verify_locations(cafile=server_cert)
context.verify_mode = ssl.CERT_REQUIRED
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_side=False, server_hostname=hostname) as socks:
        print(socks.version())
        message = input("Please enter your message: ")
        socks.send(message.encode())
        receives = socks.recv(1024)
        print(receives)
Python:
import socket
import ssl

# Генерация ключа сервера
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout server.key -out server.crt

# Генерация ключа клиента
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout client.key -out client.crt

client_cert = 'client.crt'
server_key = 'server.key'
server_cert = 'server.crt'
port = 1234

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.verify_mode = ssl.CERT_REQUIRED
context.verify_mode = ssl.CERT_REQUIRED
context.load_cert_chain(certfile=server_cert, keyfile=server_key)
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
    sock.bind(('', port))
    sock.listen(1)
    with context.wrap_socket(sock, server_side=True) as socks:
        conn, addr = socks.accept()
        print(addr)
        message = conn.recv(1024).decode()
        capitalizedMessage = message.upper()
        conn.send(capitalizedMessage.encode())
 
Последнее редактирование модератором:

TR1X

Green Team
04.04.2020
220
156
BIT
117

Введение
С появлением интернет технологий жизнь людей значительно упростилась. Все покупки, работу и другие дела можно выполнить не вставая из-за компьютера. Но вместе с этим пришли и те, кто хочет заполучить чужую конфиденциальную информацию. Чаще всего таких людей принято называть злоумышленниками. Самый простой пример их работы - это перехват трафика в публичном месте, к примеру кафе. Огромное количество людей, а значит много информации, которую можно вытащить, используя пару утилит. Но сегодня речь пойдет немного о другом. Я расскажу тебе, как защитить свою информацию при передаче, и как происходит сам процесс соединения между двумя устройствами. Обо всем по порядку.

План работы

Перед тем как перейти к работе, следует набросать план, по которому мы будем работать. Для начала я расскажу немного теории. Поговорим о работе TLS, затронем тему передачи пакетов и немного приоткроем завесу на протокол Диффи-Хеллмана. Заранее скажу, что для адекватного восприятия информации тебе потребуется ознакомится с основами передачи данных. О них я говорил в одной из своих статей. Поэтому в случаи проблем со знаниями ты знаешь куда обращаться. В практической же части статьи мы создадим TLS соединение при помощи языка Python версии 3.10, в нем будут участвовать клиент и сервер. Чтобы все корректно работало тебе потребуется любой дистрибутив Linux с утилитой openssl. Приступим к работе.

Что такое транспортный протокол

Перед тем как объяснить тебе что это такое вспомни о видах шифрования. В основном их делят на два типа: симметричное шифрование и ассиметричное. Разница лишь в генерации ключа. В первом случаи для создания надежного соединения вся информация передается напрямую. Ассиметричное же шифрование предполагает, что все требуемые данные для соединения будут генерироваться на устройствах с минимальным обменом информации.

Допустим ты имеешь публичный ключ, который состоит из 256 символов. Чтобы установить соединение со своим другом тебе следует передать ему этот ключ и получить от него точно такой же. Это как своеобразное приветствие на языке компьютеров. После этого стоит подумать о безопасности сделанного соединения и дополнительно создать приватный ключ, который никто не сможет разгадать. И здесь возникает вопрос о том, как доставить этот ключ, ведь он будет передаваться в открытом доступе и его сможет прочитать любой человек. В этом случаи обеим сторонам стоит договориться о передачи.

Для ассиметричного шифрование такое не свойственно, ведь для отправки сообщения используется совокупность открытого и закрытого ключа. И на основе этих методов мы получаем протокол защиты транспортного уровня, он же TLS (Transport Layer Security). Для установки соединения требуется обменяться двумя сообщениями, которые содержат nonce-число и публичный ключ. Чтобы тебе было понятнее я наглядно изобразил схему обмена информацией. Ее ты можешь увидеть ниже.


Посмотреть вложение 66305

При помощи таких простых обменов между сервером и клиентом устанавливается надежное соединение. После получения ключей и чисел оба устройства генерируют приватный ключ, на основе которого в дальнейшем будут передаваться данные. Также у такого типа передачи информации есть несколько небольших нюансов, о которых мы сейчас поговорим.

Чтобы не загружать твой мозг я коротко расскажу про то, что такое хеш-функция и зачем она нужна в нашей ситуации. Представь, что ты отправляешь посылку и хочешь убедиться в том, что она дойдет до получателя в целостности. На какие показатели ты можешь рассчитывать? Если это книга можно взять ее толщину или состояние влажности. В случаи с какой-то объемной посылкой можно рассчитывать на ее массу. Если прикрепить к посылке бирку с этим показателем, то на таможне люди смогут узнать повреждена посылка или нет. Так и здесь. Хеш-функция не дает злоумышленнику отправлять измененные данные в зашифрованном виде. Он может перехватить пакет и видоизменить его, но когда он дойдет до компьютера получателя, то просто будет отклонен. Хеш-функция создает одну и ту же строку фиксированной длины при использовании одних и тех же входных данных.

Также по мимо перехвата пакета не будем забывать о существовании ARP-спуфинга. Ведь в этом виде атак злоумышленник выступает как сервер и клиент одновременно. Поэтому чтобы удостовериться в том, что мы отправляем сообщение нужному человеку следует использовать центры сертификатов и подписи. Как это работает? Перед обменом данными сервер предоставляет тебе свой сертификат, документ в электронном виде, который подтверждает владельца предоставленного ключа. Работает все это дело при помощи алгоритма проверки подписи.


Центры сертификации и подписи

Теперь давай разберем, как формируется подпись. Для ее создания можно использовать алгоритм RSA. Предположим, нам требуется подписать сообщение, чтобы это сделать требуется первым делом вычислить хеш с помощью функции SHA-256, а после зашифровать все это дело закрытым ключом. В результате огромный текст из символов и будет вашей подписью. Чтобы убедиться в этом нужно все расшифровать и сравнить подпись с хэшем. Если все совпадает, то дальнейшая передача информации не прервется.

Как ты понимаешь, сертификаты тоже должны быть подтверждены. Поэтому на этот случай существует центр сертификации, который подписывает все это дело доверенной инфраструктурой открытых ключей (public key infrastructure, PKI). Давай я более подробно расскажу, что это за термин.

PKI — это набор защищенных серверов, которые подписывают и хранят заверенные копии сертификатов. Например ты должен платить за регистрацию своего сертификата в промежуточном центре сертификации (intermediate certificate authority, ICA), чтобы твой друг смог проверить сертификат во время TLS-рукопожатия.

Большинство браузеров уже автоматически настроены на использование открытого ключа ICA, поэтому они доверяют сообщениям, которые подписаны через него. Описывать процесс проверки сертификата я не стану, более подробную информацию ты сможешь найти при правильном запросе. Также бывают ситуации, когда браузеры получают сертификат ICA с неизвестным для них открытым ключом. В этих случаях браузер должен проверить его с помощью других открытых ключей.

В основном это все, что тебе следует знать о работе сертификатов, подписей и их центров. Но давай вернемся в самое начало статьи и вспомним про наше синхронное шифрование. Ведь перед передачей получатель и отправитель должны вычислить общий ключ. И для решения такой задачи на помощь приходит алгоритм Диффи-Хеллмана.

Алгоритм Диффи-Хеллмана

Итак, напомню, что для установки соединения нам требуется вычислить общий ключ. Сам алгоритм нахождения заключается в математических решениях. Их я постараюсь задействовать как можно меньше и попробую простым языком объяснить как это все работает. Первым делом разобьем всю работу на несколько этапов.
  1. Генерация общих параметров;
  2. Создание открытого и закрытого ключа;
  3. Обмен открытыми ключами и nonce-числами;
  4. Вычисление общего секретного ключа;
  5. Формирование ключа.
На первом этапе создаются общие параметры для вычисления открытого и закрытого ключа. На примере возьмем две переменные J и Y. Первая переменная будет являться генератором чисел, вторая же используется для ограничения возможных значений. Это упрощает математические расчеты без влияния на их обоснованность.

Второй этап представляет из себя выбор чисел. Получатель и отправитель определяют случайным образом номера, которые будут служить им в качестве закрытого ключа. Далее получатель и отправитель смогут вычислить открытые ключи по формулам. Если говорить простым языком, то на этом этапе идет создания всего необходимого для надежного соединения. Обычно ключи имеют размер 2048 бит, но то же АНБ для безопасности советует применять их с размером в 3072 бита. Чтобы понимать насколько это затратно представь, что один компьютер за семь часов работы способен сгенерировать 6144-битный ключ. Чтобы сгенерировать все это дело воспользуемся командой:

Bash:
openssl pkeyparam -in parametersPG.pem -text
openssl genpkey -paramfile parametersPG.pem -out PPKey.pem

Ну а чтобы оценить результат следует открыть еще одно окно консоли и напечатать в нем такие строки:

Bash:
openssl pkey -in PPKey.pem -text -noout

Как ты заметил мы сгенерировали не только ключ, но и добавили к нему параметр. За это отвечает первая команда.

Перейдем к самому интересному. На этапе создания общего секретного ключа происходит следующий алгоритм. Предположим, что получатель у нас P, а отправитель это O. Тогда, чтобы получить общий секретный ключ мы должны возвести открытый ключ P в секретный ключ O. Таким образом мы получаем общие данные, которые сгенерированы без передачи какой-либо лишней информации. Если сделать все на формулах, то получится следующие (pk - открытый ключ, sk - секретный ключ, opk - общий ключ):

Код:
OPK(O) = PK(P)^SK(O)
OPK(P) = PK(O)^SK(P)

На практике чтобы получить один ключ на основе другого тебе требуется два файла с названием key и окно терминала под рукой. Открываем его и пишем такой код:

Bash:
openssl pkeyutl -derive -inkey first_key.pem -peerkey second_key.pem -out name_output_1.bin

Теперь повторяем всю ту же процедуру, но слегка в другом порядке:

Bash:
openssl pkeyutl -derive -inkey second_key.pem -peerkey first_key.pem -out name_output_2.bin

Чтобы насладиться результатом следует воспользоваться утилитой xxd. Думаю как с ней работать ты знаешь, поэтому останавливаться на этом я не стану. Но если ключом мы можем только любоваться, то как понять, что мы все сделали верно? Для этого используем команду cmp и указываем следом два файла. В результате программа сравнит их содержимое и скажет свой вердикт по этому поводу. Ну а если экран полностью пуст, то ключи идентичны друг другу.

Финальный этап заключается в том, чтобы сформировать готовые ключи. Здесь не придется учить новые формулы или открывать для себя, что то новое. Все действие производится при помощи одной команды. Будем использовать три файла для примера шифрования.

Bash:
openssl enc -aes-256-ctr -hkdf -e -a -in rock.txt -out encrypted.txt -pass file:name_output_1.bin

В результате документ rock.txt будет зашифрован с использованием ключа name_output_1.bin, а имя итогового файла будет encrypted.txt. Готово! Теперь ты знаешь как и зачем нужен Диффи-Хеллман в TLS протоколе. На основе полученных знаний я расскажу тебе как самим создать такое соединение. Также, чтобы лучше запомнить информацию я оставил ниже визуализацию алгоритма Диффи-Хеллмана. Ну а сейчас открывай свою любимую IDE и не откладывай далеко окно консоли, ведь в скором времени мы к нему вернемся.


Посмотреть вложение 66306

TLS-соединение на Python

Первым делом я опишу тебе создание клиентской части нашего соединения. Для его работы потребуется библиотека socket и ssl. Добавляем их в наш код и следом записываем переменные для хранения ключей и сертификатов. Также не забываем про хост, куда все будет отправляться. Имя файлов можешь использовать абсолютно любые, в моем случаи скрипты будут называться secureClient и secureServer.

Python:
import socket
import ssl

client_key = 'client.key'
client_cert = 'client.crt'
server_cert = 'server.crt'
port = 1234
hostname = '127.0.0.1'

После этого нужно задать SSL-контекст. Если говорить коротко, то это класс для управления сертификатами и другими параметрами сокетов.

Python:
context = ssl.SSLContext(ssl.PROTOCOL_TLS, cafile=server_cert)
context.load_cert_chain(certfile=client_cert, keyfile=client_key)
context.load_verify_locations(cafile=server_cert)
context.verify_mode = ssl.CERT_REQUIRED
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

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

Python:
with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_side=False, server_hostname=hostname) as socks:
        print(socks.version())
        message = input("Please enter your message: ")
        socks.send(message.encode())
        receives = socks.recv(1024)
        print(receives)

На этом код клиентской части готов, но ты скорее всего задаешься вопросом, откуда взять все файлы? Для этого воспользуемся любимой утилитой openssl и сформируем всю необходимую информацию при помощи команды:

Bash:
openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout client.key -out client.crt

Теперь, чтобы ты не запутался в большом количестве флагов ниже я расписал, где и что формируется.
  • -newkey: создает новый ключ с шифрованием RSA и длинной в 3072 бита;
  • -days: ставит срок годности сертификата в нашем случаи это 365 дней;
  • -nodes: дает openssl понять, что нужно сгенерировать незашифрованный закрытый ключ;
  • -x509: задает выходной формат сертификата;
  • -keyout: задает имя выходного файла.
После этого у тебя появится вся необходимая информация для программы. Повторим то же действия, но сменив название client на server. Также одно из главных преимуществ алгоритма Диффи-Хеллмана, которое мы сейчас используем, заключается в том, что при перехвате закрытого ключа злоумышленник сможет прочитать только прошлые сообщения. По другому это называется прямой секретностью.

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

Python:
import socket
import ssl

client_cert = 'client.crt'
server_key = 'server.key'
server_cert = 'server.crt'
port = 1234

Также создаем нашу оболочку и задаем файлы отвечающие за сертификат и ключ:

Python:
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.verify_mode = ssl.CERT_REQUIRED
context.verify_mode = ssl.CERT_REQUIRED
context.load_cert_chain(certfile=server_cert, keyfile=server_key)
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

Далее по прежнему открываем соединение и вместо приглашения на отправку сообщений создаем код отвечающий за принятия всех входящих пакетов.

Python:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
    sock.bind(('', port))
    sock.listen(1)
    with context.wrap_socket(sock, server_side=True) as socks:
        conn, addr = socks.accept()
        print(addr)
        message = conn.recv(1024).decode()
        capitalizedMessage = message.upper()
        conn.send(capitalizedMessage.encode())

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

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

Подводим итоги

Я думаю, что начало нашей работы ты уже не вспомнишь из-за сильной нагрузки на мозг. К счастью я напомню тебе нашу первоначальную цель, а именно создание безопасного соединения на Python. К сожалению даже протоколы Диффи-Хеллмана не безопасны. Для их обхода потребуется огромная лаборатория серверов и метод перебора всех ключей с заранее продуманной стратегией. С другой стороны на такое безумие пошли только АНБ, а это значит, что в теории протокол до сих пор является неуязвимым для атак со стороны злоумышленников.

Python:
import socket
import ssl

# Генерация ключа сервера
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout server.key -out server.crt

# Генерация ключа клиента
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout client.key -out client.crt

client_key = 'client.key'
client_cert = 'client.crt'
server_cert = 'server.crt'
port = 1234
hostname = '127.0.0.1'

context = ssl.SSLContext(ssl.PROTOCOL_TLS, cafile=server_cert)
context.load_cert_chain(certfile=client_cert, keyfile=client_key)
context.load_verify_locations(cafile=server_cert)
context.verify_mode = ssl.CERT_REQUIRED
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_side=False, server_hostname=hostname) as socks:
        print(socks.version())
        message = input("Please enter your message: ")
        socks.send(message.encode())
        receives = socks.recv(1024)
        print(receives)
Python:
import socket
import ssl

# Генерация ключа сервера
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout server.key -out server.crt

# Генерация ключа клиента
# openssl req -new -newkey rsa:3072 -days 365 -nodes -x509 -keyout client.key -out client.crt

client_cert = 'client.crt'
server_key = 'server.key'
server_cert = 'server.crt'
port = 1234

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.verify_mode = ssl.CERT_REQUIRED
context.verify_mode = ssl.CERT_REQUIRED
context.load_cert_chain(certfile=server_cert, keyfile=server_key)
context.options |= ssl.OP_SINGLE_ECDH_USE
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
    sock.bind(('', port))
    sock.listen(1)
    with context.wrap_socket(sock, server_side=True) as socks:
        conn, addr = socks.accept()
        print(addr)
        message = conn.recv(1024).decode()
        capitalizedMessage = message.upper()
        conn.send(capitalizedMessage.encode())
Может пригодится, спасибо.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!