Работа с «КриптоПро»
Сразу хотелось бы сказать, что осуществление такого уровня защиты и функционала в мобильных приложениях в принципе не может быть столь же простым, как сверстать страницу или отправить запрос на сервер. Читать и изучать придется в любом случае, особенно если вы никогда прежде не работали с криптографией и безопасным соединением через 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 выбираем цель сборки
Далее во вкладке Build Phases выбираем Link Binary With Libraries, жмем на «+» и затем Add other, чтобы указать путь к фреймворку вручную. Находим его и выбираем Open. Фреймворк импортирован, но использовать его еще не получится.
3. Создаем в проекте папку Resources, открываем файл CPROCSP.framework, заходим в папку Resources и перетаскиваем все файлы и папки, кроме локализации (ru.lroj и en.lproj), в созданную нами папку с такими настройками:
4. Перетаскиваем в эту же папку файл лицензии, созданный в первом пункте, с настройками как на скриншоте выше.
5. Теперь переносим оставшиеся папки локализации также в Resources приложения, но с другими настройками:
Если все сделать правильно, то папки будут выглядеть так:
6. Следующим пунктом идет настройка проекта для отладки в эмуляторе. Если этого не сделать, то приложение можно будет запустить только на устройстве. Во вкладке Build Settings проекта в поле Valid Architectures оставляем только armv7. Работы по поддержке 64-битной архитектуры уже ведутся, но пока используем 32-битную. Также в Architectures записываем Optimized (armv7). Должно получиться следующее:
На этом приложенная инструкция заканчивается, но запустить приложение в итоге не получится — при компиляции вы увидите что-то вроде этого:
Непонятно, по каким причинам разработчики не дописали пару пунктов, но для корректной линковки необходимо добавить еще некоторые библиотеки и «магический флаг». Теперь подробнее.
Аналогично схеме, описанной в пункте 2, добираемся до файла CPROCSP.framework, в нем заходим в папку reader и импортируем запылившуюся там библиотеку librdrpcsc_empty.o. Следующую библиотеку добавляем из заложенных в Xcode, для этого все в том же окне в поиске вводим libz
и добавляем ее кнопкой 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:
Внутри этой папки и хранятся все контейнеры ключей. Устанавливаемые сертификаты и контейнеры хранятся и относятся к приложению, а не к ключевому хранилищу устройства, поэтому после его удаления будет необходимо установить их заново.
Далее существует два пути: написать все функции самому или использовать уже готовый контроллер, и так как его хватит для решения большинства задач, о нем и поговорим.
Фреймворк содержит в себе хедер-файл с названием PaneViewController.h. Если вы не обременены строгостью по дизайну, то можете импортировать предложенные xib-файлы (находятся в CPROCSP.framework/Resources) и инициализировать криптопанель через них — к примеру, таким образом:
<code>#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];</code>
Но так как все xib-файлы являются обычными UIView с таблицей, вы легко можете кастомизировать их под свой дизайн.
После открытия панели первое, что необходимо сделать, — это перейти во вкладку «Взаимодействие с УЦ», установить сертификат УЦ (корневой сертификат удостоверяющего центра), пройти процедуру регистрации, отправить запрос на сертификат и получить (установить) его.
После установки корневого сертификата УЦ в папке cprocsp/keys должна будет появиться папка mobile. В нее помещаются контейнеры *.000. Чтобы их установить, снова заходим в панель «КриптоПро» и на главном экране выбираем «Установить сертификаты и контейнеры». Контейнеры, содержащиеся в папке cprocsp/keys/mobile должны быть от криптопровайдера «КриптоПро» (если хотя бы один таковым не является или поврежден, то функция вернет ошибку, не установив ни один из них).
На этом установка завершена.
Подпись и проверка подписи приватным ключом
После установки корневого сертификата УЦ и контейнера можно приступать к подписи документов. Функции создания и проверки подписи можно найти в вышеупомянутом пакете для MacOS под названием CryptMsgSign. В них содержатся комментарии по каждой используемой функции. Аналогичные примеры находятся в пакете для iOS, в CreateFile/Classes/SignFie.cpp. Здесь в отличие от предыдущей реализации подписи на вход подается путь к файлу, который необходимо подписать. По причине большого количества функций разбирать каждую в отдельности мы не будем. Рассмотрим отдельные моменты, которые не так очевидны.
Функция подписи начинается с открытия хранилища сертификатов при помощи
CertOpenSystemStore:
<code>hCertStore = CertOpenSystemStore(0, "My");
if(!hCertStore) {
ret = CSP_GetLastError();
fprintf (stderr, "CertOpenSystemStore failed.");
}</code>
Здесь подразумевается не ключевое хранилище устройства, а хранилище, содержащееся в приложении в папке 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);</code>
Сообщение остается открытым до вызова 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();
}</code>
Функция 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 <CPROCSP/CPROCSP.h>
#import <CPROCSP/UrlRetriever.h>
UrlRetriever *retriever = new UrlRetriever();
И зададим настройки безопасности:
retriever->set_verify_host(true);
retriever->set_verify_server(true);</code>
После этого запишем хедеры. Так как они уже содержатся в нашем 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);</code>
В случае если мы авторизуемся без сертификата, то уже можно отсылать запрос:
retriever->retrieve_url([self.request.URL.absoluteString cStringUsingEncoding:[NSString defaultCStringEncoding]]);
</code>
И вернуть ответ в 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);
</code>
А в случае ошибки вернуть error:
self.requestCompletion(response, [[NSData alloc] init], [[NSError alloc] initWithDomain:NSURLErrorDomain code:retriever->get_error() userInfo:nil]);</code>
Данный сценарий аналогичен и для авторизации по сертификату, только перед отправкой запроса в ретривер необходимо записать отпечаток сертификата (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]];
}
</code>
Первой используется функция 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]]);</code>
Если все сделано правильно, соединение будет успешно установлено с использованием ГОСТа алгоритмов шифрования.
Полезные ссылки
MSDN — подробное описание всех функций, входящих в них параметров, а также рабочие примеры.
Форум — форум «КриптоПро», на котором сотрудники ежедневно отвечают на вопросы. Многие ответы на вопросы уже даны, и, несмотря на их давность (порою около 6 лет), они все еще могут вам помочь.
Руководство разработчика — написанное компанией руководство, которое содержит в себе перечень входящих в состав криптопровайдера функций с кратким описанием и примерами.
Описание ошибок — большой список возможных ошибок, возникающих в функциях С++, с их кодом и кратких описанием. Чтобы получить код ошибки, используйте в коде приложения функцию CSP_GetLastError, входящей в состав фреймворка. Она хранит в себе код последней полученной ошибки.
Комментарии (14)
tjomamokrenko
20.10.2017 16:02Но зачем? Есть же… Альтернативы…
DmitriyTu Автор
20.10.2017 16:11Все просто — требование заказчика. Нам уже предоставили настроенный с КриптоПро сервер.
sopov
20.10.2017 16:18Какие?
tjomamokrenko
20.10.2017 16:19CommonCrypto в iOS
henarozz
20.10.2017 18:09В CommonCrypto отсуствует реализация отечественных алгоритмов шифрования. Из Readme CommonCrypto: MD2, MD4, MD5, SHA1, SHA224, SHA256, SHA384, SHA512 are available.
DmitriyTu Автор
20.10.2017 16:20Требование — использовать КриптоПро. Поставщика СКЗИ выбрали за нас.
Mofas
20.10.2017 19:57А как теперь импортировать ключ через iTunes File Sharing?
Ведь эту функцию убрали в новой версии iTunesDmitriyTu Автор
21.10.2017 00:43Не совсем так. Эту функцию не убрали, а переименовали. Теперь она называется «Общие файлы» и найти ее можно там же — подключить устройство, нажать в левом верхнем углу на иконку телефона и в боковом меню перейти в «Общие файлы». Там будут отображены все программы, для которых включен iTunes File Sharing.
Tishka17
21.10.2017 12:23Возможно ли использование без УЦ? В частности, было бы удобно сгенерировать пару ключей на устройстве и использовать их для подписи/шифрования данных.
Ну и даже при использовании УЦ, откуда вообще у пользователя айфона под рукой комп с IE и установленными плагинами.sopov
21.10.2017 15:33Можете использовать эти функции: cpdn.cryptopro.ru/default.asp?url=content/capilite40/html/group___key_func.html
DmitriyTu Автор
21.10.2017 15:34Возможно. Ключевые контейнеры *.000 генерируются в панели КриптоПро, во вкладке «Взаимодействие с УЦ». Там указывается адрес удостоверяющего центра (по умолчанию используется https:// cryptopro.ru:5555/ui ) где будет получаться контейнер. Далее переходим во вкладку «Отправить запрос», генерируем ключ, задаем пароль для контейнера, формируется запрос. Теперь во вкладке «Получить сертификат» будет сгенерированный запрос, можно будет посмотреть сведения о сертификате и установить его. Сам контейнер *.000 можно будет найти по указанному в статье пути.
lancrypto
24.10.2017 13:30В разделе «Контейнер закрытого ключа для работы с.. .» упоминается ГОСТ Р 34.10 — 2012 как хэширование, но этот стандарт не о хэшировании. О хэшировании ГОСТ Р 34.11 — 2012.
И еще одно, включение закрытого (согласно ФЗ-63) ключа ЭП в сертификат в корне противоречит всей логике построения PKI согласно стандарта X.509. Это серьезная ошибка архитекторов-разработчиков, которая вносит сумятицу в умы широкой массы прикладников в РФ.
sopov
А как на Swift подключить и вызвать PaneViewController?
DmitriyTu Автор
Точно таким же образом, как Вы вызываете любой другой UIViewController :)