Основываясь на полученном опыте интеграции продукта в реальные проекты мне хочется отметить, что нередко разработчики для реализации серверной части предпочитают использовать поддерживающий российские криптоалгоритмы openssl.
В данной статье будет расписана типичная схема подобной интеграции, основанная на следующих сценариях использования плагина:
- Регистрация на портале (с выдачей сертификата или по имеющемуся сертификату)
- Строгая аутентификация на портале
- Электронная подпись данных и/или файлов в формате CMS
- Шифрование данных и/или файлов в формате CMS
Данные сценарии предполагают клиент-серверное взаимодействие, написание клиентских скриптов на JavaScript и соответствующих им серверных вызовов openssl.
Общие операции
Операции с устройством
Поиск подключенных устройств
Любой клиентский сценарий начинается с поиска подключенных к компьютеру USB-устройств Рутокен. В контексте данной статьи акцент делается на устройство Рутокен ЭЦП.
var devices = Array(); try { devices = plugin.enumerateDevices(); } catch (error) { console.log(error); }
При этом возвращается список идентификаторов подключенных устройств. Идентификатор представляет собой число, связанное с номером слота, к которому подключено устройство. При повторном перечислении это число может отличаться для одного и того же устройства.
Рутокен Плагин определяет все подключенные к компьютеру USВ-устройства Рутокен ЭЦП, Рутокен PINPad, Рутокен WEB. Поэтому следующим шагом следует определить тип устройства.
Получение информации об устройстве
Для определения типа устройства следует использовать функцию getDeviceInfo с параметром TOKEN_INFO_DEVICE_TYPE. Значение этой константы содержится в объекте плагина.
var type; try { type = plugin.getDeviceInfo(deviceId, plugin.TOKEN_INFO_DEVICE_TYPE); } catch (error) { console.log(error); } switch (type) { case plugin.TOKEN_TYPE_UNKNOWN: message = "Неизвестное устройство"; break; case plugin.TOKEN_TYPE_RUTOKEN_ECP: message = "Рутокен ЭЦП"; break; case plugin.TOKEN_TYPE_RUTOKEN_WEB: message = "Рутокен Web"; break; case plugin.TOKEN_TYPE_RUTOKEN_PINPAD_2: message = "Рутокен PINPad"; break; }
Также с помощью функции getDeviceInfo можно получить:
- модель устройства
- метку устройства
- серийный номер устройства
- информацию о том, залогинен ли пользователь на устройство
Смена PIN-кода
var options = {}; try { plugin.changePin(deviceId, "12345678", "12345671", options); } catch (error) { console.log(error); }
Здесь первым параметром выступает старый PIN-код, а вторым новый PIN-код.
Работа с сертификатами
1. На токене могут храниться 3 категории сертификатов:
- пользовательские (константа в плагине CERT_CATEGORY_USER)
Это сертификаты, связанные с закрытым ключом пользователя. Применяются, например, для подписи CMS/PKCS#7, аутентификации пользователя в протоколе TLS.
Если сертификат импортируется как пользовательский, то при импорте будет произведен поиск в устройстве соответствующего ему закрытого ключа. Если такой ключ будет найден, то сертификат будет «привязан» к этому ключу. Если ключ найден не будет, то вернется ошибка. - корневые (константа в плагине CERT_CATEGORY_CA)
Это сертификаты издателей сертификатов, применяющиеся для проверки подписи под сертификатами. Подобная проверка подписи (построение цепочки доверия) позволяет определить, доверяет ли пользователь подписи другого пользователя. Например, в функции плагина verify есть режим проверки сертификата, на котором было подписано сообщение. При этом используется хранилище корневых сертификатов на токене, созданное импортом корневых сертификатов на токен. - другие (константа в плагине CERT_CATEGORY_OTHER)
Это сертификаты, которые не связаны с закрытым ключом и не являются корневыми.
2. Для чтения сертификатов, хранящихся на устройстве, не требуется авторизация на устройство.
var certs = Array(); try { certs = plugin.enumerateCertificates(deviceId, plugin.CERT_CATEGORY_USER); } catch (error) { console.log(error); }
3. Сертификат можно экспортировать в PEM-формате:
var certpem; try { certpem = plugin.getCertificate(deviceId, certId); } catch (error) { console.log(error); }
Получится примерно такая строка:
-----BEGIN CERTIFICATE----- MIIBmjCCAUegAwIBAgIBATAKBgYqhQMCAgMFADBUMQswCQYDVQQGEwJSVTEPMA0G A1UEBxMGTW9zY293MSIwIAYDVQQKFBlPT08gIkdhcmFudC1QYXJrLVRlbGVjb20i MRAwDgYDVQQDEwdUZXN0IENBMB4XDTE0MTIyMjE2NTEyNVoXDTE1MTIyMjE2NTEy NVowEDEOMAwGA1UEAxMFZmZmZmYwYzAcBgYqhQMCAhMwEgYHKoUDAgIjAQYHKoUD AgIeAQNDAARADKA/O1Zw50PzMpcNkWnW39mAJcTehAhkQ2Vg7bHkIwIdf7zPe2Px HyAr6lH+stqdACK6sFYmkZ58cBjzL0WBwaNEMEIwJQYDVR0lBB4wHAYIKwYBBQUH AwIGCCsGAQUFBwMEBgYpAQEBAQIwCwYDVR0PBAQDAgKkMAwGA1UdEwEB/wQCMAAw CgYGKoUDAgIDBQADQQD5TY55KbwADGKJRK+bwCGZw24sdIyayIX5dn9hrKkNrZsW detWY3KJFylSulykS/dfJ871IT+8dXPU5A7WqG4+ -----END CERTIFICATE-----
4. Сертификат можно распарсить вызовом функции parseCertificate и получить из него DN Subject, DN Issuer, расширения, значение открытого ключа, подпись, серийный номер, срок действия и т.п.
5. Сертификат можно записать на устройство.
var certpem = "-----BEGIN CERTIFICATE----- MIIBmjCCAUegAwIBAgIBATAKBgYqhQMCAgMFADBUMQswCQYDVQQGEwJSVTEPMA0G A1UEBxMGTW9zY293MSIwIAYDVQQKFBlPT08gIkdhcmFudC1QYXJrLVRlbGVjb20i MRAwDgYDVQQDEwdUZXN0IENBMB4XDTE0MTIyMjE2NTEyNVoXDTE1MTIyMjE2NTEy NVowEDEOMAwGA1UEAxMFZmZmZmYwYzAcBgYqhQMCAhMwEgYHKoUDAgIjAQYHKoUD AgIeAQNDAARADKA/O1Zw50PzMpcNkWnW39mAJcTehAhkQ2Vg7bHkIwIdf7zPe2Px HyAr6lH+stqdACK6sFYmkZ58cBjzL0WBwaNEMEIwJQYDVR0lBB4wHAYIKwYBBQUH AwIGCCsGAQUFBwMEBgYpAQEBAQIwCwYDVR0PBAQDAgKkMAwGA1UdEwEB/wQCMAAw CgYGKoUDAgIDBQADQQD5TY55KbwADGKJRK+bwCGZw24sdIyayIX5dn9hrKkNrZsW detWY3KJFylSulykS/dfJ871IT+8dXPU5A7WqG4+ -----END CERTIFICATE-----"; try { plugin.importCertificate(deviceId, certpem, plugin.CERT_CATEGORY_USER); } catch (error) { console.log(error); }
6. Вызовом функции deleteCertificate можно удалить сертификат с токена.
Работа с ключевыми парами ГОСТ Р 34.10-2001
1. Для получения декрипторов ключевых пар, хранящихся на устройстве, требуется ввод PIN-кода. Следует понимать, что само значение закрытого ключ получено быть не может, так как ключ является неизвлекаемым.
var keys = Array(); try { plugin.login(deviceId, "12345678"); keys = plugin.enumerateKeys(deviceId, null); } catch (error) { console.log(error); }
2. Для генерации ключевой пары требуется ввод PIN-кода. При генерации ключа параметры могут быть выбраны из набора:
- A: id-GostR3410-2001-CryptoPro-A-ParamSet
- B: id-GostR3410-2001-CryptoPro-B-ParamSet
- C: id-GostR3410-2001-CryptoPro-C-ParamSet
- XA: id-GostR3410-2001-CryptoPro-XchA-ParamSet
- XB: id-GostR3410-2001-CryptoPro-XchB-ParamSet
var options = {}; var keyId; try { keyId = plugin.generateKeyPair(deviceId, "A", null, options); } catch (error) { console.log(error); }
3. С помощью функции deleteKeyPair ключевая пара может быть удалена с токена.
Конфигурирование openssl
Openssl поддерживает российские криптоалгоритмы, начиная с версии 1.0. Для того, чтобы их использовать, в openssl требуется подгружать engine gost. В большинстве дистрибутивов openssl эта библиотека присутствует. Чтобы engine подгружалась, можно прописать ее в конфигурационном файле openssl:
[openssl_def]
engines = engine_section
[engine_section]
gost = gost_section
[gost_section]
engine_id = gost
default_algorithms = ALL
Если конфигурационный файл openssl не расположен в стандартном месте, то путь к нему можно задать через переменную окружения OPENSSL_CONF.
Другим вариантом подгрузки engine gost является ее передача в параметрах командной строки утилиты openssl.
Если engine gost не расположена в стандартном месте, то через переменную окружения OPENSSL_ENGINES можно задать путь к директории, в которой openssl будет ее искать.
Для получения информации о том, успешен ли был вызов утилиты openssl или нет, с возможностью уточнения ошибки, требуется парсить stdout и stderror. В конце статьи приведена ссылка на PHP-скрипт, который использует данную утилиту.
Теперь перейдем к реализации законченных пользовательских сценариев.
Регистрация на портале
Сертификат выдается при регистрации в системе
- Получаем список подключенных к компьютеру устройств Рутокен ЭЦП
- Генерируем ключевую пару ГОСТ Р 34.10-2001 на выбранном Рутокен ЭЦП
- Cоздаем запрос PKCS#10 на сертификат для сгенерированной ключевой пары
- Отправляем запрос на сервер
- На сервере создаем сертификат, привязываем к аккаунту (сам сертификат или его дескриптор). Следует отметить, что дескрипторы сертификатов, полученные при вызове функции enumerateCertificates, являются уникальными и неизменными
- Отправляем сертификат на клиент
- На клиенте визуализируем полученный сертификат
- Импортируем полученный сертификат в Рутокен ЭЦП
Последовательность вызовов в клиентском скрипте будет следующей: