Полная цепочка взаимодействия: Рутокен ЭЦП 2.0 + rtPKCS11ECP.dll + КриптоПро + .NET

  • Михаил
  • 8 мин. на прочтение
  • 6
  • 15 Apr 2025
  • 15 Apr 2025

Ниже подробно описана архитектура, потоки данных и взаимодействие всех компонентов при подписании данных с использованием Рутокен ЭЦП 2.0.


Общая архитектура (схема)

┌─────────────────────────────────────────────────────┐
│  Ваше .NET-приложение (RutokenSigner)                                                                   │
│  • Pkcs11Interop 5.3.0 (HighLevelAPI)                                                                           │
│  • BouncyCastle.Cryptography 2.6.2 (хеширование)                                                    │
│  • System.Text.Json (формирование JSON)                                                                  │
└──────────────┬──────────────────────────────────────┘
              │ вызовы через интерфейс ISession
              ▼
┌─────────────────────────────────────────────────────┐
│  PKCS#11 Middleware: rtPKCS11ECP.dll                                                                        │
│  • Реализация стандарта PKCS#11 v2.40                                                                      │
│  • Vendor-расширения ТК-26 (0xd43210xx)                                                                 │
│  • Маршрутизация команд к драйверу токена                                                           │
│  • Преобразование .NET-типов ↔ нативные структуры                                             │
└──────────────┬──────────────────────────────────────┘
              │ вызовы через C API (C_SignInit, C_Sign...)
              ▼
┌─────────────────────────────────────────────────────┐
│  Драйвер уровня ядра: rtDevice.sys / librt*.so                                                            │
│  • Управление USB-устройством                                                                                  │
│  • Буферизация команд и ответов                                                                               │
│  • Шифрование канала связи с токеном (опционально)                                           │
└──────────────┬──────────────────────────────────────┘
              │ протокол обмена с чипом
              ▼
┌─────────────────────────────────────────────────────┐
│  🔐 Аппаратный токен: Рутокен ЭЦП 2.0                                                                     │
│  • Защищённый микроконтроллер (SmartCard chip)                                                 │
│  • Невыводимые закрытые ключи (внутри чипа)                                                       │
│  • Аппаратная реализация ГОСТ-алгоритмов:                                                            │
│    - ГОСТ Р 34.11-2012 (хеширование)                                                                        │
│    - ГОСТ Р 34.10-2012 (подпись)                                                                                 │
│  • Защищённая память для сертификатов и ключей                                                 │
└─────────────────────────────────────────────────────┘


Детальная цепочка вызовов при подписании

Шаг 1: Загрузка PKCS#11 библиотеки

// Ваше приложение
var factories = new Pkcs11InteropFactories();
using IPkcs11Library pkcs11 = factories.Pkcs11LibraryFactory
   .LoadPkcs11Library(factories, "rtPKCS11ECP.dll", AppType.MultiThreaded);

Что происходит:

  1. LoadPkcs11Library() вызывает LoadLibrary() (Windows) / dlopen() (Linux)
  2. Загружается rtPKCS11ECP.dll в адресное пространство процесса
  3. Вызывается C_Initialize() — инициализация внутреннего состояния библиотеки
  4. Создаётся объект IPkcs11Library, который является обёрткой над нативными функциями

Шаг 2: Поиск слотов и токенов

var slots = pkcs11.GetSlotList(SlotsType.WithTokenPresent);

Поток данных:

.NET → Pkcs11Interop → rtPKCS11ECP.dll → rtDevice.sys → USB-стек → Рутокен
                                                                                                                                 ↓
.NET ← Pkcs11Interop ← rtPKCS11ECP.dll ← rtDevice.sys ← USB-стек ← Рутокен

Что возвращается:

  • Список слотов (физических/виртуальных портов)
  • Для каждого слота: CK_TOKEN_INFO (метка, серийный номер, версия прошивки)

Шаг 3: Открытие сессии

using ISession session = slots[0].OpenSession(SessionType.ReadWrite);

Что происходит внутри rtPKCS11ECP.dll:

  1. Вызов C_OpenSession(slotID, CKF_RW_SESSION, ...)
  2. Библиотека проверяет, что токен в слоте доступен
  3. Создаётся контекст сессии (хранит состояние: логин, активные операции)
  4. Устанавливается соединение с токеном через драйвер

Шаг 4: Аутентификация (ввод PIN)

session.Login(CKU.CKU_USER, "12345678");

Поток выполнения:

1. .NET: session.Login() 
2. Pkcs11Interop: преобразует строку PIN в byte[] + вызывает C_Login()
3. rtPKCS11ECP.dll: 
  • Проверяет формат PIN (длина, кодировка)
  • Формирует APDU-команду VERIFY для токена
  • Отправляет через драйвер
4. Рутокен:
  • Принимает PIN
  • Сравнивает с эталоном в защищённой памяти
  • Возвращает статус (успех/ошибка)
5. При успехе: в сессии выставляется флаг "авторизован"

⚠️ Важно: После 3-5 неверных попыток токен блокируется (зависит от настроек).


Шаг 5: Поиск ключа для подписи

var keyFilter = new List<IObjectAttribute>
{
   f.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_PRIVATE_KEY),
   f.ObjectAttributeFactory.Create(CKA.CKA_ID, certId),
   f.ObjectAttributeFactory.Create(CKA.CKA_SIGN, true)
};
var keys = session.FindAllObjects(keyFilter);

Как работает поиск на токене:

1. Pkcs11Interop формирует шаблон поиска (template)
2. rtPKCS11ECP.dll преобразует его в внутренние структуры
3. Вызывается C_FindObjectsInit() → C_FindObjects() → C_FindObjectsFinal()
4. Рутокен сканирует свою память объектов:
  • Каждый объект имеет тип (ключ, сертификат, данные)
  • У каждого объекта есть атрибуты (CKA_*)
  • Поиск идёт по точному совпадению указанных атрибутов
5. Возвращаются дескрипторы (handles) найденных объектов

Структура объекта ключа на Рутокене:

┌─────────────────────────────┐
│ Объект: закрытый ключ                                 │
├─────────────────────────────┤
│ • CKA_CLASS = CKO_PRIVATE_KEY
│ • CKA_KEY_TYPE = CKK_GOSTR3410_2012_256
│ • CKA_ID = [A1 B2 C3 D4]    ← связка с сертификатом
│ • CKA_LABEL = "Key for signing"
│ • CKA_SIGN = true           ← можно подписывать
│ • CKA_EXTRACTABLE = false   ← ключ нельзя извлечь!
│ • Ключевые данные: [ЗАШИФРОВАНЫ, внутри чипа]
└─────────────────────────────┘


Шаг 6: Хеширование данных (BouncyCastle)

byte[] hash = ComputeGostHash(data); // Gost3411_2012_256Digest

Почему хешируем на стороне приложения?

 

Вариант

Где хешируется

Плюсы

Минусы

На ПК (BouncyCastle)

В .NET-приложении

• Быстро • Не зависит от токена • Легко отлаживать

• Не сертифицировано для юр. значимости

На токене

Внутри Рутокена

• Сертифицировано • Ключ никогда не покидает токен

• Медленнее • Требует механизма с хешированием

Ваш код использует первый вариант — это нормально для тестов и внутренних систем.


Шаг 7: Подпись на токене

IMechanism mech = factories.MechanismFactory.Create((CKM)0xd4321006UL);
byte[] signature = session.Sign(mech, privateKey, hash);

Детальный поток выполнения:

1. .NET: session.Sign(mech, keyHandle, hash)
2. Pkcs11Interop:
  • Преобразует IMechanism в CK_MECHANISM (нативная структура)
  • Преобразует IObjectHandle в CK_OBJECT_HANDLE (uint)
  • Вызывает C_SignInit() + C_Sign()
3. rtPKCS11ECP.dll:
  • Проверяет, что механизм 0xd4321006 поддерживается
  • Проверяет, что ключ имеет CKA_SIGN=true
  • Формирует команду для токена:
    ┌─────────────────────────────┐
    │ Команда: SIGN               │
    │ • Механизм: 0xd4321006      │
    │ • Дескриптор ключа: 0x1234  │
    │ • Данные: [32 байта хеша]   │
    └─────────────────────────────┘
4. Рутокен (внутри чипа):
  • Находит ключ по дескриптору в защищённой памяти
  • Загружает ключевые параметры в крипто-движок
  • Выполняет алгоритм ГОСТ Р 34.10-2012:
    - Вход: 32-байтный хеш
    - Выход: 64-байтная подпись (R || S, по 32 байта)
  • Возвращает подпись
5. rtPKCS11ECP.dll → Pkcs11Interop → .NET: byte[64]

Важно: Закрытый ключ никогда не покидает токен. На ПК передаётся только хеш и возвращается только подпись.


Шаг 8: Завершение сессии

session.Logout(); // Вызывает C_Logout()
// using-блок вызывает Dispose() → C_CloseSession() → C_Finalize()

Что очищается:

  • Сбрасывается флаг авторизации в сессии
  • Освобождаются дескрипторы объектов
  • Закрывается соединение с токеном
  • При C_Finalize() — освобождение ресурсов библиотеки

🔐 Где и как хранятся ключи на Рутокене

┌─────────────────────────────────────┐
│ Защищённая память Рутокен ЭЦП 2.0   │
├─────────────────────────────────────┤
│                                     │
│  ┌──────────────────────────────┐   │
│  │ Область: Закрытые ключи      │   │
│  │ • Доступ: только после PIN   │   │ 
│  │ • Экспорт: запрещён          │   │
│  │ • Формат: PKCS#8 + шифрование│   │
│  └──────────────────────────────┘   │
│                                     │
│  ┌─────────────────────────────┐    │
│  │ Область: Сертификаты        │    │
│  │ • Доступ: публичный         │    │
│  │ • Формат: DER (X.509)       │    │
│  │ • Связка: по CKA_ID         │    │
│  └─────────────────────────────┘    │
│                                     │
│  ┌─────────────────────────────┐    │
│  │ Область: Пользовательские   │    │
│  │ данные (опционально)        │    │
│  └─────────────────────────────┘    │
│                                     │
└─────────────────────────────────────┘

Связка "сертификат ↔ ключ":

  • Оба объекта имеют одинаковый атрибут CKA_ID (байтовый массив)
  • При поиске ключа вы сначала находите сертификат, берёте его CKA_ID, ищете ключ с таким же CKA_ID
  • Это стандарт PKCS#11, работает на всех токенах

🔄 Взаимодействие с КриптоПро CSP

Если на системе установлен КриптоПро CSP, возможны два сценария:

Сценарий А: КриптоПро НЕ используется (только Рутокен)

Приложение → rtPKCS11ECP.dll → Рутокен

Сценарий Б: КриптоПро использует Рутокен как хранилище ключей

Приложение → CryptoPro CSP → rtPKCS11ECP.dll → Рутокен

Как это работает:

  1. КриптоПро регистрирует свой PKCS#11-модуль (cryptopki.dll)
  2. При создании контейнера КриптоПро может выбрать Рутокен как целевое устройство
  3. КриптоПро вызывает функции rtPKCS11ECP.dll для:
    • Создания ключей (C_GenerateKeyPair)
    • Записи сертификатов (C_CreateObject)
    • Подписи (C_Sign)
  4. Ваш .NET-код может работать напрямую с rtPKCS11ECP.dll, минуя КриптоПро

⚠️ Важно: rtPKCS11ECP.dll — это библиотека Рутокена, а не КриптоПро.
КриптоПро имеет свою библиотеку: cryptopki.dll (Windows) / libcryptoki.so (Linux).


Диагностика: как проверить каждый уровень

1. Проверка USB-подключения

# Windows
Get-PnpDevice | Where-Object {$_.FriendlyName -like "*Rutoken*"}
# Linux
lsusb | grep -i rutoken
dmesg | grep -i usb

2. Проверка драйвера и библиотеки

# Файл библиотеки
Test-Path "C:\Windows\System32\rtPKCS11ECP.dll"
# Версия драйвера (через реестр)
Get-ItemProperty "HKLM:\SOFTWARE\Aktiv Co.\Rutoken" | Select-Object Version

3. Проверка PKCS#11-интерфейса

# Утилита pkcs11-tool (из пакета opensc или Pkcs11Interop)
pkcs11-tool --module rtPKCS11ECP.dll --list-slots
pkcs11-tool --module rtPKCS11ECP.dll --list-mechanisms | findstr /i "gost"

4. Проверка токена из .NET (минимальный тест)

var f = new Pkcs11InteropFactories();
using var lib = f.Pkcs11LibraryFactory.LoadPkcs11Library(f, "rtPKCS11ECP.dll", AppType.MultiThreaded);
var slots = lib.GetSlotList(SlotsType.WithTokenPresent);
Console.WriteLine($"Слотов: {slots.Count}");
if (slots.Count > 0)
{
   var info = slots[0].GetTokenInfo();
   Console.WriteLine($"Метка: {info.Label}");
   Console.WriteLine($"Серийный: {info.SerialNumber}");
}

🚨 Типичные проблемы и решения

 

Проблема

Уровень

Причина

Решение

FileNotFoundException

Загрузка DLL

Библиотека не найдена

Установить драйверы Рутокен или задать путь через RUTOKEN_PKCS11

CKR_DEVICE_ERROR

Драйвер/токен

Токен не отвечает

Переподключить токен, проверить USB-порт, обновить драйвер

CKR_PIN_INCORRECT

Аутентификация

Неверный PIN

Ввести правильный (по умолчанию: 12345678), сбросить через утилиту

CKR_MECHANISM_INVALID

Механизм

Токен не поддерживает 0xd4321006

Использовать 0xd4321008 или 0x80000008 (зависит от прошивки)

CKR_KEY_HANDLE_INVALID

Поиск ключа

Дескриптор устарел

Пересоздать сессию, не хранить handles между сессиями

CKR_USER_NOT_LOGGED_IN

Авторизация

Попытка использовать ключ без логина

Вызвать Login() перед операциями с ключом

Подпись не верифицируется

Криптография

Передан хеш вместо данных (или наоборот)

Для механизма 0xd4321008 передавать data, для 0xd4321006hash


Сравнение: где что выполняется

 

Операция

Где выполняется

Почему так

Формирование JSON

.NET-приложение

Бизнес-логика, не связана с крипто

Хеширование (ГОСТ)

.NET (BouncyCastle)

Быстрее, проще отлаживать; для юр. значимости — на токене

Поиск ключа

Рутокен

Ключи хранятся внутри, поиск по защищённой памяти

Подпись

Рутокен (аппаратно)

Закрытый ключ не должен покидать токен (требование безопасности)

Проверка подписи

.NET или внешняя система

Публичный ключ доступен всем, проверка не требует токена


Безопасность: что защищено, что нет

 

Компонент

Защищённость

Комментарий

Закрытый ключ

🔒 Максимальная

Никогда не покидает чип токена, неэкстрагируемый

PIN-код

🔒 Высокая

Передаётся по зашифрованному каналу, не логируется

Хеш данных

⚠️ Средняя

Передаётся в открытом виде (но это не секрет)

Подпись

✅ Публичная

Предназначена для передачи и проверки

Исходные данные

⚠️ Зависит от приложения

Шифруйте канал передачи, если данные конфиденциальны

Сессия PKCS#11

🔒 Высокая

Дескрипторы валидны только в рамках сессии


Рекомендации по архитектуре

Для производства:

// 1. Вынесите настройки токена в конфиг
public static class TokenSettings
{
   public static string LibraryPath { get; set; } = 
       Environment.GetEnvironmentVariable("TOKEN_PKCS11") 
       ?? (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
           ? @"C:\Windows\System32\rtPKCS11ECP.dll"
           : "/usr/lib/librtpkcs11ecp.so");
   
   public static ulong PreferredMechanism { get; set; } = 0xd4321006UL;
   public static bool UseTokenForHashing { get; set; } = false; // true для юр. значимости
}
// 2. Добавьте логирование операций
using var logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger<Program>();
logger.LogInformation("Загрузка библиотеки: {Path}", TokenSettings.LibraryPath);
// 3. Обрабатывайте все исключения специфично
catch (Pkcs11Exception ex) when (ex.RV == CKR.CKR_MECHANISM_INVALID)
{
   logger.LogWarning("Механизм {Mech} не поддерживается, пробуем альтернативы...", SIGN_MECHANISM);
   // Попробовать другой механизм
}

Для юридически значимой подписи:

  1. Используйте хеширование на токене (механизм 0xd4321008)
  2. Замените BouncyCastle на сертифицированный провайдер (КриптоПро)
  3. Оберните подпись в CMS/PKCS#7 с указанием всех атрибутов
  4. Ведите журнал операций (кто, когда, что подписал)

Итоговая схема потока данных при подписании

[.NET-приложение]
      │
      ▼
[Pkcs11Interop: HighLevelAPI]
      │ • Преобразование типов
      │ • Управление сессиями
      ▼
[rtPKCS11ECP.dll]
      │ • Реализация PKCS#11 v2.40
      │ • Vendor-расширения ТК-26
      │ • Маршрутизация к драйверу
      ▼
[rtDevice.sys / librt*.so]
      │ • USB-коммуникация
      │ • Буферизация, таймауты
      ▼
[Рутокен ЭЦП 2.0 (чип)]
      │ • Защищённая память
      │ • Аппаратный ГОСТ-движок
      │ • Невыводимые ключи
      ▼
[Возврат: 64-байтная подпись]
      │
      ▼
[.NET-приложение: формирование итогового JSON]

 


💡 Ключевой вывод: Ваш код корректно реализует всю цепочку. Главное — убедиться, что:

  1. Драйверы и библиотека установлены
  2. Механизм 0xd4321006 поддерживается вашей прошивкой
  3. Ключ на токене имеет CKA_SIGN=true и правильный тип (CKK_GOSTR3410_2012_256)