Импорт закрытого ключа Rutoken ECP 2.0

Добрый день, исправил код текущей ветки OpenSC для импорта закрытых ключей GOST R3410 в рутокен, изменения вносились согласно документации на pkcs11 (openoasis) и rutoken.

Ссылка на форк библиотеки, оттуда нужен только pkcs11_tool
https://github.com/Lirein/OpenSC

Вызываю так:
./src/tools/pkcs11-tool --module /usr/lib/librtpkcs11ecp.so -l -E gost -y privkey -w key.der --slot 0 --id 65536
Пробовал указывать другие механизмы, эффект одинаковый:
error: PKCS11 function C_CreateObject failed: rv = CKR_TEMPLATE_INCONSISTENT (0xd1)

В документации указано что ошибка модет возникать в том числе при не поддерживаемом типе ключей для импорта.

Код патча:

diff --git a/src/tools/pkcs11-tool.c b/src/tools/pkcs11-tool.c
index 8aa1cff..d4fe730 100644
--- a/src/tools/pkcs11-tool.c
+++ b/src/tools/pkcs11-tool.c
@@ -50,6 +50,7 @@
 #include <openssl/asn1t.h>
 #include <openssl/rsa.h>
 #include <openssl/pem.h>
+#include <openssl/engine.h>
 #if OPENSSL_VERSION_NUMBER >= 0x00908000L && !defined(OPENSSL_NO_EC) && !defined(OPENSSL_NO_ECDSA)
 #include <openssl/ec.h>
 #include <openssl/ecdsa.h>
@@ -179,6 +180,7 @@ static const struct option options[] = {
     { "usage-sign",        0, NULL,        OPT_KEY_USAGE_SIGN },
     { "usage-decrypt",    0, NULL,        OPT_KEY_USAGE_DECRYPT },
     { "usage-derive",    0, NULL,        OPT_KEY_USAGE_DERIVE },
+    { "engine",        1, NULL,        'E' },
     { "write-object",    1, NULL,        'w' },
     { "read-object",    0, NULL,        'r' },
     { "delete-object",    0, NULL,        'b' },
@@ -244,6 +246,7 @@ static const char *option_help[] = {
     "Specify 'sign' key usage flag (sets SIGN in privkey, sets VERIFY in pubkey)",
     "Specify 'decrypt' key usage flag (RSA only, set DECRYPT privkey, ENCRYPT in pubkey)",
     "Specify 'derive' key usage flag (EC only)",
+    "Set SSL engine provider",
     "Write an object (key, cert, data) to the card",
     "Get object's CKA_VALUE attribute (use with --type)",
     "Delete an object (use with --type cert/data/privkey/pubkey/secrkey)",
@@ -291,6 +294,7 @@ static int        opt_slot_index_set = 0;
 static CK_MECHANISM_TYPE opt_mechanism = 0;
 static int        opt_mechanism_used = 0;
 static const char *    opt_file_to_write = NULL;
+static const char *    opt_engine = NULL;
 static const char *    opt_object_class_str = NULL;
 static CK_OBJECT_CLASS    opt_object_class = -1;
 static CK_BYTE        opt_object_id[100], new_object_id[100];
@@ -529,6 +533,7 @@ int main(int argc, char * argv[])
 #endif
     int need_session = 0;
     int opt_login = 0;
+    int do_set_engine = 0;
     int do_init_token = 0;
     int do_init_pin = 0;
     int do_change_pin = 0;
@@ -536,7 +541,7 @@ int main(int argc, char * argv[])
     int action_count = 0;
     int do_generate_random = 0;
     CK_RV rv;
-
+    ENGINE *eng = NULL;
 #ifdef _WIN32
     char expanded_val[PATH_MAX];
     DWORD expanded_len;
@@ -547,24 +552,8 @@ int main(int argc, char * argv[])
         util_fatal("Cannot set FMODE to O_BINARY");
 #endif
 
-#ifdef ENABLE_OPENSSL
-#if (OPENSSL_VERSION_NUMBER >= 0x00907000L && OPENSSL_VERSION_NUMBER < 0x10100000L) || defined(LIBRESSL_VERSION_NUMBER)
-    OPENSSL_config(NULL);
-#endif
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
-    OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS
-        | OPENSSL_INIT_ADD_ALL_CIPHERS
-        | OPENSSL_INIT_ADD_ALL_DIGESTS
-        | OPENSSL_INIT_LOAD_CONFIG,
-        NULL);
-#else
-    /* OpenSSL magic */
-    OpenSSL_add_all_algorithms();
-    OPENSSL_malloc_init();
-#endif
-#endif
     while (1) {
-        c = getopt_long(argc, argv, "ILMOTa:bd:e:hi:klm:o:p:scvf:ty:w:z:r",
+        c = getopt_long(argc, argv, "ILMOTa:bd:e:hi:klm:o:p:scvf:ty:w:z:rE:",
                         options, &long_optind);
         if (c == -1)
             break;
@@ -612,6 +601,12 @@ int main(int argc, char * argv[])
             opt_file_to_write = optarg;
             action_count++;
             break;
+        case 'E':
+            need_session |= NEED_SESSION_RW;
+            do_set_engine = 1;
+            opt_engine = optarg;
+            action_count++;
+            break;
         case 'r':
             need_session |= NEED_SESSION_RO;
             do_read_object = 1;
@@ -841,6 +836,26 @@ int main(int argc, char * argv[])
             util_print_usage_and_die(app_name, options, option_help, NULL);
         }
     }
+#ifdef ENABLE_OPENSSL
+#if (OPENSSL_VERSION_NUMBER >= 0x00907000L && OPENSSL_VERSION_NUMBER < 0x10100000L) || defined(LIBRESSL_VERSION_NUMBER)
+    OPENSSL_config(NULL);
+#endif
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+    OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS
+        | OPENSSL_INIT_ADD_ALL_CIPHERS
+        | OPENSSL_INIT_ADD_ALL_DIGESTS
+        | OPENSSL_INIT_LOAD_CONFIG,
+        NULL);
+#else
+    /* OpenSSL magic */
+        if(do_set_engine) {
+          eng = ENGINE_by_id(opt_engine);
+          ENGINE_set_default(eng, ENGINE_METHOD_ALL);
+        }
+    OpenSSL_add_all_algorithms();
+    OPENSSL_malloc_init();
+#endif
+#endif
     if (optind < argc) {
         util_fatal("invalid option(s) given");
     }
@@ -2415,6 +2430,8 @@ parse_ec_pkey(EVP_PKEY *pkey, int private, struct gostkey_info *gost)
    public keys (-type pubkey) and data objects (-type data). */
 static int write_object(CK_SESSION_HANDLE session)
 {
+        const CK_BYTE GOST_HASH_PARAMSET_OID[] = {0x06, 0x07, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1e, 0x01};
+        CK_BYTE hash_paramset_encoded_oid[9];
     CK_BBOOL _true = TRUE;
     CK_BBOOL _false = FALSE;
     unsigned char contents[MAX_OBJECT_SIZE + 1];
@@ -2634,12 +2651,16 @@ static int write_object(CK_SESSION_HANDLE session)
             n_privkey_attr++;
         }
         else if (pk_type == NID_id_GostR3410_2001)   {
-            type = CKK_GOSTR3410;
+printf("Write gost key\n");
+                        memcpy(hash_paramset_encoded_oid, GOST_HASH_PARAMSET_OID, sizeof(GOST_HASH_PARAMSET_OID));
 
+            type = CKK_GOSTR3410;
             FILL_ATTR(privkey_templ[n_privkey_attr], CKA_KEY_TYPE, &type, sizeof(type));
             n_privkey_attr++;
             FILL_ATTR(privkey_templ[n_privkey_attr], CKA_GOSTR3410_PARAMS, gost.param_oid.value, gost.param_oid.len);
             n_privkey_attr++;
+                        FILL_ATTR(privkey_templ[n_privkey_attr], CKA_GOSTR3411_PARAMS, hash_paramset_encoded_oid, sizeof(hash_paramset_encoded_oid));
+            n_privkey_attr++;
             FILL_ATTR(privkey_templ[n_privkey_attr], CKA_VALUE, gost.private.value, gost.private.len);
             /* CKA_VALUE of the GOST key has to be in the little endian order */
             rv = sc_mem_reverse(privkey_templ[n_privkey_attr].pValue, privkey_templ[n_privkey_attr].ulValueLen);
@@ -2735,6 +2756,7 @@ static int write_object(CK_SESSION_HANDLE session)
             n_pubkey_attr++;
         }
         else if (pk_type == NID_id_GostR3410_2001) {
+printf("Write gost public key");
             type = CKK_GOSTR3410;
 
             FILL_ATTR(pubkey_templ[n_pubkey_attr], CKA_KEY_TYPE, &type, sizeof(type));
@@ -2824,6 +2846,7 @@ static int write_object(CK_SESSION_HANDLE session)
     }
 
     if (n_privkey_attr) {
+                printf("Create object with gost key\n");
         rv = p11->C_CreateObject(session, privkey_templ, n_privkey_attr, &privkey_obj);
         if (rv != CKR_OK)
             p11_fatal("C_CreateObject", rv);

Re: Импорт закрытого ключа Rutoken ECP 2.0

Добрый день, lirein.
Рутокен ЭЦП 2.0 не поддерживает импорт закрытых ключей, это требование регулятора. По очевидными причинам это ограничение сделано не на уровне PKCS11, а внутри самого устройства.

Если вам нужен импорт закрытого ключа, вы можете использовать Рутокен ЭЦП PKI.
Вы для каких целей хотите сделать подобный импорт?

Re: Импорт закрытого ключа Rutoken ECP 2.0

Спасибо за ответ, иморт закрытых ключей стандарта RSA осуществляется с помощью pkcs11-tool успешно, сертификаты ключа проверки электронной подписи так же привязываются, в том числе через веб портал Центра регистрации Rutoken.
Не осуществляется импорт ключа GOST R3410, нужные модификации для чтения ключа OpenSC произведены (возможность использования альтернативных криптопровайдеров, в т.ч. КриптоПро или Рутокен).

Задача следующая, в настоящий момент времени невозможно осуществление доступа на портал Госуслуг с использованием плагина Госуслуг IFC с использованим СКЗИ КриптоПро, в частности при подключении модуля pkcs11 в конфигурации плагина IFC, при попытке обращения к нему в журнал падает ошибка "Too many events on token" и сертификаты не читаются, тем не менее сам КриптоПро видится и даже рапортует о наличии в слоте сертификатов. Похоже на недоработку со стороны КриптоПро.

Экспортировать и разобрать закрытый ключ из контейнера криптопро можно как шатаными средствами (несовместимо с OpenSSL) так и через специализированную утилиту от Лисси. Rutoken PKI под ОС Windows позволяет импорт только закрытых ключей RSA включая сертификат из файлов формата pkcs12, контейнер с ключем ГОСТ не импортируется. В ОС Linux импорт сертификатов можно осуществить через NSS (например через плагин pkcs11 в Firefox) из контейнеров pkcs12, но ГОСТ сертификат при этом импортировать невозможно, даже с использованием обозревателя CryptoFox.

Собственно нужно перенести сертификат из контейнера КриптоПро на Рутокен, для обеспечения полноценной работы в ОС GNU\Linux.

Re: Импорт закрытого ключа Rutoken ECP 2.0

Добрый день, lirein.
Давайте разбираться.
Как я понял, конечная цель всех этих действий - полноценная работа в ОС GNU\Linux на сайте госуслуг.
Сайт госуслуги работает с КЭП, которая создана по ГОСТ 34.10. Для работы с КЭП сайт госуслуги использует плагин IFCPlugin от компании Ростелеком. Этот плагин умеет работать с 2 разными интерфейсами - CAPI и PKCS11. КриптоПро CSP - криптопровайдер, который реализует интерфейс CAPI. librtpkcs11ecps.so - библиотека, которая реализует интерфейс PKCS11. Эти интерфейсы не совместимы между собой.
У Вас есть токен с сертификатом в формате КриптоПро CSP. Увидеть этот сертификат через интерфейс PKCS11 невозможно. Похоже, что у Вас сертификат с извлекаемым ключом. Поэтому Вам удалось успешно сделать импорт. Теперь Вы хотите импортировать этот ключ на токен, через интерфейс PKCS11 и использовать в Linux через тот же PKCS11.

Сделать такое с Рутокен ЭЦП 2.0 у Вас не получится. Импорт ГОСТ ключей на Рутокен ЭЦП 2.0 запрещен по требованиям регулятора, сделано это внутри устройства. Редактирование OpenSC тут никак не поможет. Единственный вариант это пойти в УЦ и перевыпустить сертификат, произнеся магическую фразу "Как для ЕГАИС". Тогда токен сам сгенерирует неизвлекаемые ключи и сертификат в формате PKCS11. Тогда госуслуги будут работать в Linux, но ключи будут защищены от копирования, неизвлекаемые из устройства.

Вариант номер 2. Импортировать полученный из КриптоПро сертификат и ключи на несертифицированное устройство Рутокен ЭЦП PKI. Да, Панель управления Рутокен такой импорт не поддерживает, но сам токен поддерживает. И импорт средствами того же opensc должен сработать. Если не сработает - пишите - разберемся, поможем. Будет работать в Linux на госуслугах. Хотя с юридической точки зрения это нарушение, так как в вашем сертификате прописано средство подписи СКЗИ КриптоПро CSP, и использовать подпись через другие средства запрещено.

(2017-08-25 20:25:02 отредактировано lirein)

Re: Импорт закрытого ключа Rutoken ECP 2.0

Владимир, спасибо за ответ. Значит ограничение накладывается самим токеном.
К слову у КриптоПро есть pkcs11 интерфейс, но похоже только для Mac и Linux платформ: /opt/cprocsp/lib/amd64/libcppkcs11.so
Во всяком случае Firefox, Thunderbird и OpenSC с ним работают, проблема только у IFC плагина госуслуг (описана выше). Попинаю КриптоРро и Ростелеком на предмет совместимости. Может сдвинется с мертвой точки.

У меня сертификат выдан УЦ Такском, без привязки к конкретному криптопровайдеру, но т.к. 1С умеет работать только с КриптоПро (Linux), то и сертификат установлен с использованием данного криптопровайдера. Изначально использовался Rutoken Light, но он работает либо с КриптоПро, либо с PKCS, третьего не дано.