What is openssl in android

Делаем MitM с помощью openssl на Android

Мотивация

В русскоязычном интернете трудно найти информацию об API-библиотеке OpenSSL. Большое внимание уделяется использованию консольных команд для манипуляции с самоподписанными сертификатами для веб-серверов или OpenVPN-серверов.

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

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

Отдельно стоит отметить сетевую составляющую. Если сертификат есть и просто лежит на диске, он бесполезен.

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

Данная статья — компиляция моего опыта по работе с библиотекой OpenSSL при реализации клиент-серверного приложения. Описанные в ней функции будут работать как на десктопе, так и на Android-устройствах. К статье прилагается репозиторий с кодом на C/C++ для того, чтобы вы могли увидеть работу описываемых функций.

При изучении новой библиотеки или технологии я стараюсь решать проблемы с помощью нового функционала. В данном случае попробуем сделать MITM
для перехвата трафика к HTTPS-серверу.

Сформируем требования к программе:
Ожидать подключения по порту (SSL-сервер)
При появлении входящего подключения:

  • Подключиться к HTTPS-серверу
  • Прочитать запрос клиента к серверу
  • Передать прочитанные от клиента данные серверу
  • Прочитать ответ сервера
  • Передать ответ сервера клиенту
  • Сбросить соединения

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

Разработка будет вестись на Ubuntu, прочий инструментарий: компилятор GCC 5.4.0, OpenSSL 1.0.2, curl 7.52.1, CMake 3.8.1 (единственный не из пакетов).

Для отправки запросов к нашему приложению будем использовать curl из консоли. Поскольку нам нужно указать CA-сертификат, команда будет выглядеть так:

Указание заголовка Host требуется для того, чтобы curl корректно составил HTTP-запрос. Без этого сервер ответит ошибкой.

Начало и завершение работы

Для работы с библиотекой OpenSSL ее нужно инициализировать. Используйте следующий код:

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

Контекст

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

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

При этом библиотека автоматически выберет максимально безопасный протокол, поддерживаемый клиентом и сервером.

Вот пример создания контекста для клиента:

Для корректного удаления контекста следует использовать функцию SSL_CTX_free .

Мне не очень нравится использовать SSL_CTX_free каждый раз, когда контекст необходимо удалить. Можно использовать умные указатели с указанием функции удаления или обернуть структуру в класс RAII:

Работа с ошибками

Большинство функций библиотеки OpenSSL возвращают 1 как признак успешного выполнения. Вот обычный код с проверкой на возникновение ошибки:

Читайте также:  Поиск андроид по имей

Однако иногда этого недостаточно, а иногда необходимо более подробное описание проблемы. Для этого OpenSSL использует в каждом потоке отдельную очередь сообщений. Чтобы извлечь код ошибки из очереди, следует использовать функцию ERR_get_error() .

Сам по себе код ошибки не очень понятен пользователю, поэтому можно использовать функцию ERR_error_string для получения строкового представления о коде ошибки. Если функция возвращает 0, это значит, что ошибки нет.

Вот пример получения строки с описанием ошибки по коду ошибки:

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

Стоит отметить, что для каждого отдельного потока создается отдельная очередь сообщений об ошибке.

Ключи

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

Создание

Для хранения пары закрытый/открытый ключ в OpenSSL используется структура EVP_PKEY . Данная структура создается следующим образом:

Подробнее о EVP можно прочитать здесь.

Обратная для EVP_PKEY_new функция EVP_PKEY_free освобождает память и удаляет структуру EVP_PKEY .

Теперь необходимо подготовить BIGNUM структуру для генерации RSA (подробнее об этой структуре можно узнать здесь):

Функция BN_set_word устанавливает размер для структуры BIGNUM . Допустимыми являются значения RSA_3 и RSA_F4 , последний — предпочтительнее.

Настал черед для генерации ключей. Для этого нужно создать структуру RSA :

Теперь сама генерация ключей:

4096 это размер ключа, который мы хотим получить.

Заканчиваем генерацию ключей записью новых ключей в структуру EVP_PKEY :

PEM-формат

PEM — достаточно простой формат для хранения ключей и сертификатов. Он представляет собой текстовый файл, в котором последовательно хранятся записи вида:

Тут стоит отметить, что количество символов —— в начале заголовка и в конце, а также в закрывающей строке должно быть одинаковым.

Более подробно этот формат описан тут:
— RFC1421 Part I: Message Encryption and Authentication Procedures
— RFC1422 Part II: Certificate-Based Key Management
— RFC1423 Part III: Algorithms, Modes, and Identifiers
— RFC1424 Part IV: Key Certification and Related Services

Запись ключей в PEM-формате

Допустим, у нас есть пара открытый/закрытый ключ в структуре EVP_PKEY, тогда для записи их в файл следует использовать функции PEM_write_PrivateKey и PEM_write_PUBKEY .

Вот пример использования этих функций:

Стоит дать некоторые пояснения относительно функции

, где const EVP_CIPHER *enc — это указатель на алгоритм шифрования для шифрования закрытого ключа перед его сохранением.
Например, EVP_aes_256_cbc() значит «AES with a 256-bit key in CBC».

Алгоритмов шифрования очень много, и всегда можно
подобрать что-то по душе. Соответствующие определения можно найти в openssl/evp.h .
unsigned char *kstr ожидает получить указатель на строку с паролем для шифрования ключа, а int klen — длину этой строки.

Если заданы kstr и klen , то параметры cb и u игнорируются, где:— cb — это указатель на функцию вида:

— buf — указатель на буфер для записи пароля
— size — максимальный размер пароля (т.е. размер буфера)
— rwflag равен 0 при чтении и 1 при записи

Результат выполнения функции — длина пароля или 0 в случае возникновения ошибки.

Параметр void *u для обеих функций используется для передачи дополнительных данных. Например, как указатель на окно для GUI приложения.

Загрузка ключей из PEM-файла

Загрузка ключей происходит при помощи функций PEM_read_PrivateKey и PEM_read_PUBKEY . Обе функции имеют одинаковые параметры и возвращаемое значение:

где:
— FILE *fp — открытый для чтения файловый дескриптор
— EVP_PKEY **x — структура, которая должна быть перезаписана
— pem_password_cb *cb — функция для получения пароля расшифровки ключа
— void *u — строка с паролем ключа, завершающаяся \0

Вот пример функции для получения пароля для расшифровки ключа:

Вот пример того, как можно загрузить не зашифрованный закрытый ключ из файла:

Загрузка ключей из памяти

Иногда бывает удобно хранить ключ или сертификат как константу в программе. Для таких случаев можно использовать структуру типа BIO. Эта структура и связанные с ней функции повторяют функционал ввода-вывода для FILE .

Читайте также:  Скин эдитор для андроид

Вот так можно загрузить ключ из памяти:

Запрос сертификата

Теперь, когда мы умеем создавать ключи, посмотрим, как создавать сертификаты. Сертификаты могут быть самоподписанными или иметь подпись удостоверяющего центра. Для получения сертификата, подписанного удостоверяющим центром, требуется создать запрос сертификата (CSR) и отправить его удостоверяющему центру. В ответ он пришлет подписанный сертификат.

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

Создание

Certificate Signing Request (CSR) — это сообщение или запрос, который создатель сертификата посылает удостоверяющему центру (CA) и который содержит в себе информацию о публичном ключе, стране выпуска, а также цифровую подпись создателя.

Для создания CSR нам понадобится ключ EVP_PKEY , созданный ранее. Все начинается с выделения памяти под структуру CSR:

Обратной функцией к X509_REQ_new является X509_REQ_free .

Теперь нужно задать версию сертификата. В данном случае версия равна 2:

По стандарту X.509 эта версия должна быть на единицу меньше версии сертификата. Т.е. для версии сертификата 3 следует использовать число 2.

Теперь будем задавать данные создателя запроса. Будем использовать следующие поля:

С — двухбуквенный код страны, например RU
ST — область, в нашем случае Moscow
L — город, снова Moscow
O — организация, например Taigasystem
CN — доменное имя, для нас будет taigasystem.com

Вот так эти поля задаются в запрос:

Заметим, что сначала мы получаем структуру X509_NAME из структуры CSR запроса и задаем значения для нее.

Теперь нужно задать публичный ключ для этого запроса:

Последний штрих — подпись запроса:

В отличие от других функций OpenSSL, X509_REQ_sign возвращает
размер подписи в байтах, а не 1, при успешном завершении, и 0 — в случае ошибки.

Теперь запрос сертификата готов.

Сохранение CSR в файл

Сохранить CSR в файл довольно просто. Необходимо открыть файл, а затем — вызвать функцию PEM_write_X509_REQ :

В результате мы получим такой текст в файле server.csr:

Загрузка CSR из файла

Для загрузки CSR следует использовать функции:

Первая загружает CSR из файла, вторая позволяет загрузить CSR из памяти.

Параметры данных функция аналогичны загрузке ключей из PEM-файла, за
исключением pem_password_cb и u , которые игнорируются.

Сертификаты

X.509 сертификат

X.509 — это одна из вариаций языка ASN.1, стандартизованная в rfc2459.

Более подробно о формате X.509 можно прочитать здесь и здесь.

Генерация сертификата без CSR

Можно сгенерировать сертификат без использования CSR. Это пригодится для создания CA.

Для генерации сертификата без CSR нам понадобится пара открытый/закрытый ключ в структуре EVP_PKEY . Начинаем с выделения памяти под структуру сертификата:

Обратной для X509_new является X509_free .

Сертификат создается точно так же, как и CSR-запрос, с одной лишь разницей — помимо версии и данных издателя, требуется задать серийный номер сертификата.

Также нужно использовать другие функции для доступа к данным сертификата:
— X509_set_version вместо X509_REQ_set_version
— X509_get_subject_name вместо X509_REQ_get_subject_name
— X509_set_pubkey вместо X509_REQ_set_pubkey
— X509_sign вместо X509_REQ_sign

Таким образом отличить по именам, для каких объектов предназначены те или иные функции, становится довольно просто.

Теперь можно установить серийный номер сертификата:

Для каждого нового сертификата необходимо создавать новый серийный номер.

Теперь пришло время установить время жизни сертификата. Для этого устанавливается два параметра — начало и конец жизни сертификата:

Началом жизни сертификата будет момент его выпуска — значение 0 для функции X509_get_notBefore . Конец жизни сертификата задается функцией X509_get_notAfter .

Последний штрих — подпись сертификата при помощи закрытого ключа:

Здесь есть интересная особенность: функция X509_sign возвращает размер подписи в байтах, если все прошло успешно, и 0 — в случае ошибки. Иногда функция возвращает ноль, даже если ошибки нет. Поэтому здесь приходится вводить дополнительную проверку на ошибку.

Генерация сертификата с CSR

Для генерации сертификата по CSR нам нужен закрытый ключ CA для подписи сертификата, сертификат CA для задания данных издателя и сам CSR-запрос.

Создание самого сертификата и установка версии и номера такие же, как и для сертификата без CSR. Разница появляется, когда нужно извлечь данные издателя из CSR-запроса и установить их в сертификат:

Читайте также:  Vendor id как узнать android

После этого нужно уставить данные издателя сертификата. Для этого требуется сертификат CA:

Видно, что данные из CSR мы устанавливаем при помощи X509_set_subject_name , а данные CA — при помощи X509_set_issuer_name .

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

Теперь можно установить серийный номер сертификата:

Последний штрих — следует подписать сертификат при помощи закрытого ключа CA:

После подписи наш новый сертификат готов.

Сохранение X.509 сертификата

Сохранение происходит довольно просто:

Загрузка X.509 сертификата

Загрузка происходит при помощи двух функций:

Сетевая часть

Клиент

Подключение к хосту при помощи SSL-сокетов не очень отличается от обычного TCP-подключения.

Сначала нужно создать TCP-подключение к серверу:

Теперь нужно создать клиентский SSL-контекст:

Теперь нужно установить полученный сокет в SSL-структуру. Она получается из контекста:

Последний штрих — само подключение:

Чтение данных

Для чтения данных из SSL-сокета нужно использовать функцию:

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

Т.е. для чтения можно использовать такую конструкцию:

В результате в буфере будут находиться уже декодированные данные.

Запись данных

Для записи используется функция:

На входе она получает указатель на SSL-структуру, которая будет осуществлять запись, сами данные и их размер.

Вот пример такой записи:

Сервер

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

Начнем с подготовки контекста:

Документация говорит нам, о том, что выбор SSLv23_server_method позволит библиотеке самостоятельно определить максимально безопасную версию протокола из поддерживаемых клиентом.

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

Загрузку X.509 сертификата и загрузку ключей мы рассмотрели чуть раньше, поэтому считаем, что у нас уже есть пара этих структур.

Установим для серверного контекста сертификат и ключ сертификата:

Наш сервер готов принимать входящие соединения. Пусть этим занимается обычный accept . Нас будет интересовать полученный от этой функции сокет.

Начинаем с того, что для каждого такого сокета, нам нужна новая SSL-структура:

Теперь устанавливаем в эту структуру наш сокет:

Сам механизм рукопожатия:

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

После установки соединения мы можем использовать описанные ранее функции ввода-вывода для чтения и записи данных в сокет.

Пример работы

Сборка программы

Для сборки примера к статье нужно использовать команды:

Запуск

В папке сборки (build) следует выполнить:

При этом в этой же папке появится файл ca.crt — созданный программой
сертификат.

Чтобы проверить его работоспособность, нужно выполнить

Первым параметром мы задаем используемый CA Сертификат, -v показывает нам все переданные и принятые сообщения. -H «Host: taigasystem.com» нужен, чтобы заметить в GET-запросе заголовок Host. Без этого параметра программа отработает, но в ответ на запрос мы получим 404-ю ошибку.

Вывод curl

Вот вывод запроса curl при запросе через нашу программу (не полный):

Видим следующее: во-первых, наш сертификат прочитан и принят; во-вторых, сервер ответил на запрос и передает ответ.

Вывод программы

После запуска программы и подключения к ней curl видим такой (неполный) вывод:

Т.е. очевидно, что мы получили GET-запрос от клиента (curl) и ответ на него от сервера.

Ссылки

Огромная благодарность за исследования tomasloh

Источник

Оцените статью