Защищаем мобильное приложение с помощью «КриптоПро»: пошаговая инструкция

2694
#Разработка 17 декабря 2017

Работа с «КриптоПро»

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

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

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

Также придется поработать с С++ и самостоятельно заботиться о выделении и очистке памяти. Стоит отметить, что статья написана при актуальной версии «КриптоПро CSP 4.0», поэтому описанные ниже пакеты для разработчиков в будущем могут отличаться.

Импорт «КриптоПро» в мобильное приложение

Первый шаг для использования «КриптоПро» в своем приложении — это внедрить фреймворк, скачанный с сайта компании. В пакете для iOS помимо самого фреймворка содержатся также три примера — создание электронной подписи (ЭЦП), создание ssl-тоннеля и пример, позволяющий собрать браузер, обладающий функциональностью создания и проверки ЭЦП на веб-страницах. Подробное описание содержится внутри примеров, в файлах Readme. На их основе легче понять алгоритм действий. Очень поможет пакет для MacOS: в нем содержится много полезных примеров с подробными комментариями. После его установки все примеры будут находиться на диске в разделе opt/cprocsp/src/doxygen/CSP.

Инструкция по импортированию самого фреймворка находится в CPROCSP.framework в файле ReadMe, разберем его по пунктам.

1. От вас требуется зайти в консоль и прописать путь к скаченному фреймворку, далее запустить утилиту, как описано ниже:

cd /Users/agima/КриптоПро/ios-uni/CPROCSP.framework/

./ SetApplicationLicense 40400-W0037-EKVQK-9YDNG-D3F67 license.enc

(указанный ключ прилагается в файле, затем в этой же директории будет создан файл лицензии license.enc).

2. Прежде чем импортировать фреймворк, советуем перенести его файл CPROCSP.framework в папку с проектом. Так вы защитите себя от случайного переноса фреймворка в другую папку, из-за чего «слетят» пути в Xсode.

Теперь к импорту: открываем Xсode, в боковой панели слева выбираем проект и в списке targets выбираем цель сборки

tabl.png

Далее во вкладке Build Phases выбираем Link Binary With Libraries, жмем на «+» и затем Add other, чтобы указать путь к фреймворку вручную. Находим его и выбираем Open. Фреймворк импортирован, но использовать его еще не получится.

tabl.png

3. Создаем в проекте папку Resources, открываем файл CPROCSP.framework, заходим в папку Resources и перетаскиваем все файлы и папки, кроме локализации (ru.lroj и en.lproj), в созданную нами папку с такими настройками:

tabl.png

4. Перетаскиваем в эту же папку файл лицензии, созданный в первом пункте, с настройками как на скриншоте выше.

5. Теперь переносим оставшиеся папки локализации также в Resources приложения, но с другими настройками:

tabl.png

Если все сделать правильно, то папки будут выглядеть так:

tabl.png

6. Следующим пунктом идет настройка проекта для отладки в эмуляторе. Если этого не сделать, то приложение можно будет запустить только на устройстве. Во вкладке Build Settings проекта в поле Valid Architectures оставляем только armv7. Работы по поддержке 64-битной архитектуры уже ведутся, но пока используем 32-битную. Также в Architectures записываем Optimized (armv7). Должно получиться следующее:

tabl.png

На этом приложенная инструкция заканчивается, но запустить приложение в итоге не получится — при компиляции вы увидите что-то вроде этого:

tabl.png

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

Аналогично схеме, описанной в пункте 2, добираемся до файла CPROCSP.framework, в нем заходим в папку reader и импортируем запылившуюся там библиотеку librdrpcsc_empty.o. Следующую библиотеку добавляем из заложенных в Xcode, для этого все в том же окне в поиске вводим libz

tabl.png

и добавляем ее кнопкой Add.

Но этого тоже будет недостаточно, останется еще одна ошибка.

Для ее решения в любое место проекта поместите флаг

extern bool USE_CACHE_DIR;

bool USE_CACHE_DIR = false;

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

/private/var/root/Documents/cprocsp/keys 

Если поставить true, то они будут храниться в закрытом месте:

/private/var/root/Library/Caches/cprocsp/keys/

На этом импорт фреймворка окончен, и можно запустить приложение на эмуляторе.

Контейнер закрытого ключа для работы с «КриптоПро»

На момент написания статьи «КриптоПро CSP 4.0» не поддерживает работу с сертификатами *.pfx, которые содержат и публичный, и приватный ключ. Необходимо использовать контейнеры *.000, которые можно получить только при использовании «КриптоПро»; их формат нигде не описан, и записываются они, как правило, на ключевых носителях, токенах (флешках, дискетах). В проводнике они выглядят как папки, которые содержат в себе один ключ, разбитый на файлы header.key, masks.key, name.key и primary.key.

Также у компании есть тестовый удостоверяющий центр (УЦ), на котором можно сформировать ключи и получить сертификат УЦ. Здесь необходимо дополнить, что для корректной работы страницы оформления запроса сертификата необходимо использовать Internet Explorer (или любой другой браузер с установленным плагином, который можно скачать в тестовом УЦ), и на компьютере должно быть установлено CSP, иначе на странице оформления заявки на получение сертификата будут недоступны необходимые алгоритмы хеширования ГОСТ Р 34.10-2012.

Установка контейнера закрытого ключа в приложение

Как пишут сами сотрудники «КриптоПро», есть три варианта установки контейнера на устройство:

  • Через iTunes File Sharing.
  • Использовать отдельное приложение для записи файлов на iOS, типа iExplorer.
  • Написать собственный экспорт ключа в блоб (PRIVATEKEYBLOB) и импортировать его в коде приложения. Пример такого способа находится в вышеупомянутом пакете для MacOS под названием EncryptKey/DecryptKey.

Мы использовали первый вариант — как самый быстрый и удовлетворяющий требованиям заказчика. Именно для его реализации при импорте был выбран флаг USE_CACHE_DIR = false; если iTunes File Sharing вами использоваться не будет или вы хотите скрыть от пользователя возможность заглянуть в папку с ключами, то лучше скройте ее, выставив флаг true.

После первого запуска с установленным фреймворком в папке Documents приложения будет создана папка cprocsp:

tabl.png

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

Далее существует два пути: написать все функции самому или использовать уже готовый контроллер, и так как его хватит для решения большинства задач, о нем и поговорим. Фреймворк содержит в себе хедер-файл с названием PaneViewController.h. Если вы не обременены строгостью по дизайну, то можете импортировать предложенные xib-файлы (находятся в CPROCSP.framework/Resources) и инициализировать криптопанель через них — к примеру, таким образом:

#import “CPROCSP/PaneViewControler.h”
…

PaneViewController *CPROPane;
    
    if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
        CPROPane = [[PaneViewController alloc] initWithNibName:@"PaneViewController" bundle:nil];
    else
        CPROPane = [[PaneViewController alloc] initWithNibName:@"PaneViewControllerIPhone" bundle: nil];
    
    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:CPROPane];
    [navController setModalPresentationStyle:UIModalPresentationFullScreen];
    
    [self presentViewController:navController animated:true completion:nil];

Но так как все xib-файлы являются обычными UIView с таблицей, вы легко можете кастомизировать их под свой дизайн.

tabl.png

После открытия панели первое, что необходимо сделать, — это перейти во вкладку «Взаимодействие с УЦ», установить сертификат УЦ (корневой сертификат удостоверяющего центра), пройти процедуру регистрации, отправить запрос на сертификат и получить (установить) его.

tabl.png

После установки корневого сертификата УЦ в папке cprocsp/keys должна будет появиться папка mobile. В нее помещаются контейнеры *.000. Чтобы их установить, снова заходим в панель «КриптоПро» и на главном экране выбираем «Установить сертификаты и контейнеры». Контейнеры, содержащиеся в папке cprocsp/keys/mobile должны быть от криптопровайдера «КриптоПро» (если хотя бы один таковым не является или поврежден, то функция вернет ошибку, не установив ни один из них). На этом установка завершена.

Подпись и проверка подписи приватным ключом

После установки корневого сертификата УЦ и контейнера можно приступать к подписи документов. Функции создания и проверки подписи можно найти в вышеупомянутом пакете для MacOS под названием CryptMsgSign. В них содержатся комментарии по каждой используемой функции. Аналогичные примеры находятся в пакете для iOS, в CreateFile/Classes/SignFie.cpp. Здесь в отличие от предыдущей реализации подписи на вход подается путь к файлу, который необходимо подписать. По причине большого количества функций разбирать каждую в отдельности мы не будем. Рассмотрим отдельные моменты, которые не так очевидны.

Функция подписи начинается с открытия хранилища сертификатов при помощи CertOpenSystemStore:

hCertStore = CertOpenSystemStore(0, "My");
	if(!hCertStore) {
		ret = CSP_GetLastError();
		fprintf (stderr, "CertOpenSystemStore failed.");
}

Здесь подразумевается не ключевое хранилище устройства, а хранилище, содержащееся в приложении в папке cprocsp/users/mobile/stores. Функция работает так, что если такого хранилища не будет найдено, она его создаст, поэтому вам об этом можно не заботиться. При использовании панели PaneViewController будет создано и использовано хранилище MY.sto. После получения указателя на сертификат, выделения из него приватного ключа и прочих инициализаций процедура подписи начинается с функции CryptMsgOpenToEncode, которая открывает сообщение для кодирования и возвращает дескриптор открытого сообщения:

if(hMsg = CryptMsgOpenToEncode(TYPE_DER,        		// Encoding type
						0,                        		// Flags
						CMSG_SIGNED,           // Message type
						&SignedMsgEncodeInfo,    // Pointer to structure
						NULL,                    // Inner content object ID
						NULL))                   // Stream information (not used)
    {
		printf("The message to be encoded has been opened. \n");
    } else {
		ret = CSP_GetLastError();
		fprintf (stderr, "OpenToEncode failed");
    }
…

if(hMsg)
     CryptMsgClose(hMsg);

Сообщение остается открытым до вызова CryptMsgClose. Здесь стоит обратить внимание на используемые флаг (второй параметр): если выставить 0, как в примере, то подпись будет присоединенная (attached); для получения отсоединенной (detached) подписи необходимо выставить флаг CMSG_DETACHED_FLAG. После этого подписываемые данные помещаются в сообщение и полученный блоб можно вывести в отдельный файл.

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

if(CryptMsgControl(hMsg,                       // Handle to the message
			   0,                          // Flags
			   CMSG_CTRL_VERIFY_SIGNATURE, // Control type
			   pSignerCertificateInfo))    // Pointer to the CERT_INFO
    {
		printf("\nSignature was VERIFIED.\n");
    } else {
		printf("\nThe signature was NOT VERIFIED.\n");
		ret = CSP_GetLastError();
    }

Функция CSP_GetLastError() возвращает код ошибки и краткое описание.

Авторизация при помощи приватного ключа

Для авторизации с использованием приватного ключа нам понадобится сам ключ, установленный вышеописанным образом, установленный корневой сертификат УЦ, настроенный сервер с «КриптоПро» и с добавленным в доверенные нашим ключом. Сервер должен поддерживать работу по TLS версии 1.2 для всех устройств с iOS 9 и выше, а также иметь ГОСТовые cipher suites от «КриптоПро».

Для подключения к серверу мы создали отдельный менеджер и переписали для его NSURLSessionDataTask функцию resume. Так как в нашем приложении есть возможность авторизоваться как по http, так и по https, мы переключались между двумя менеджерами, отвечающими за разное подключение. Создание отдельного менеджера необходимо для того, чтобы использовать cipher suites от «КриптоПро». Если во время хендшейка в Client Hello вы их не положите, то когда с вами «поздоровается» сервер, TLS-соединение будет провалено, так как будет выбран неверный cipher suite.

Для выявления причин ошибок и получения подробной информации по подключению может пригодиться такая программа, как WireShark. В ней есть возможность посмотреть все этапы TLS-соединения и достаточно информации для анализа. Также в ней вы можете посмотреть, какие cipher suites отправляете и какие поддерживает сервер.

В качестве менеджера, который будет отправлять нужные cipher suites, используется UrlRetriever, входящий в состав фреймворка. Открыв хедер, вы увидите множество понятных функций для отправки запроса, но не сразу очевидно, как и чем пользоваться, тем более что документации по этому вы не найдете ни на сайте, ни на форуме. И в самом файле также нет никаких комментариев, за исключением одной //TODO, которые оставили разработчики :)

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

Для начала создадим сам ретривер, так как для каждого запроса нам необходимо будет очищать в нем информацию. Вначале функции resume создадим экземпляр класса:

#import 
#import 


UrlRetriever *retriever = new UrlRetriever();


И зададим настройки безопасности:


retriever->set_verify_host(true);
retriever->set_verify_server(true);

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

__block UrlHeaders headers;

[self.request.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
        NSString *result = [NSString stringWithFormat:@"%@: %@", key, obj];
        std::string *string = new std::string([result cStringUsingEncoding:[NSString defaultCStringEncoding]]);
        
        headers.push_back(*string);
    }];

retriever->set_headers(headers);

Теперь записываем сам запрос:

BYTE *requestBytes = (BYTE *)[self.request.HTTPBody bytes];
retriever->set_postmessage(requestBytes, self.request.HTTPBody.length);

В случае если мы авторизуемся без сертификата, то уже можно отсылать запрос:

retriever->retrieve_url([self.request.URL.absoluteString cStringUsingEncoding:[NSString defaultCStringEncoding]]);

И вернуть ответ в completion-блок:

NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:self.request.URL statusCode:retriever->get_code() HTTPVersion:@"HTTP/1.1" headerFields:self.request.allHTTPHeaderFields];

self.requestCompletion(response, [NSData dataWithBytes:retriever->get_data() length:retriever->get_data_len()], nil);

А в случае ошибки вернуть error:

self.requestCompletion(response, [[NSData alloc] init], [[NSError alloc] initWithDomain:NSURLErrorDomain code:retriever->get_error() userInfo:nil]);

Данный сценарий аналогичен и для авторизации по сертификату, только перед отправкой запроса в ретривер необходимо записать отпечаток сертификата (thumbprint). Существует множество вариантов получения отпечатка — мы покажем самый правильный (на наш взгляд). Из примеров для подписи возьмем реализацию некоторых функций, таких как CertOpenStore, для открытия хранилища и CertEnumCertificatesInStore — для получения дескриптора сертификата PCCERT_CONTEXT. После этого пишем функцию для получения отпечатка в NSSting:

- (NSString *)getCertificateHashFromCert:(PCCERT_CONTEXT)certContext {
    
    BYTE *pvData = NULL;
    DWORD cbSize = 0;
    DWORD cbHash = 0;
    
    if(!CertGetCertificateContextProperty(certContext, CERT_SHA1_HASH_PROP_ID, NULL, &cbSize))
        NSLog(@"CertGetCertificateContextProperty error %u", CSP_GetLastError());
    if (pvData) {
        free(pvData);
    }

    pvData = malloc(cbSize);
    cbHash = cbSize;

    if(!CertGetCertificateContextProperty(certContext, CERT_SHA1_HASH_PROP_ID, pvData, &cbSize))
        NSLog(@"CertGetCertificateContextProperty error %u", CSP_GetLastError());
    
    DWORD dest;
    if (!CryptBinaryToString(pvData, cbHash, 0, NULL, &dest))
        NSLog(@"CryptBinaryToString error: %u", CSP_GetLastError());
    
    LPWSTR buf = malloc(dest * sizeof(TCHAR));
    
    if (!CryptBinaryToString(pvData, cbHash, 0, buf, &dest))
        NSLog(@"CryptBinaryToString error: %u", CSP_GetLastError());
    
    return [[NSString alloc] initWithData:[NSData dataWithBytes:buf length:dest] encoding:[NSString defaultCStringEncoding]];
}

Первой используется функция CertGetCertificateContextProperty c параметром CERT_SHA1_HASH_PROP_ID, который говорит функции о параметре, который необходимо получить из сертификата, в данном случае — SHA1 hash. Функция вызывается два раза, первый без подстановки буфера, вместо него ставится NULL. Это делается для получения требуемого размера памяти, которую нужно выделить под буфер. Во втором использовании подставляется буфер теперь уже известного размера. Чтобы преобразовать полученные данные из массива байт в строку, используется функция CryptBinaryToString — тоже два раза (по тем же причинам). После этого вы получите красивое содержимое вида:

—----BEGIN CERTIFICATE-----
Хэш
—---- END CERTIFICATE-----/code

Но для ретривера нужен другой формат. Для получения необходимого отпечатка в CryptBinaryToString во флаги (третий параметр) требуется записать флаг CRYPT_STRING_HEX. Полученная в результате строка и будет являться отпечатком, который далее ставится в ретривер:

retriever->set_client_cert([thumbPrint cStringUsingEncoding:[NSString defaultCStringEncoding]]);

Если все сделано правильно, соединение будет успешно установлено с использованием ГОСТа алгоритмов шифрования.



Контент-хаб

0 / 0
+7 495 981-01-85 + Стать клиентом
Услуги Кейсы Контент-хаб