PKCS#11 для самых маленьких / Хабр

Основной этап

Основной этап можно разделить на работу со слотами и работу внутри сессии

Этап работы со слотами

Слот — это дескриптор виртуального интерфейса, куда подключен токен. Конкретно в нашем примере весь этап работы со слотами уместился в определение одной функции get_slot_list:

int get_slot_list(CK_SLOT_ID_PTR* slots_ptr, CK_ULONG_PTR slotCount)
{
    CK_RV rv;
    int errorCode = 1;

    /*************************************************************************
    * Получить количество слотов c подключенными токенами                    *
    *************************************************************************/
    rv = functionList->C_GetSlotList(CK_TRUE, NULL_PTR, slotCount);
    CHECK_AND_LOG(" C_GetSlotList (number of slots)", rv == CKR_OK, rvToStr(rv), exit);

    CHECK_AND_LOG(" Checking available tokens", *slotCount > 0, " No tokens available", exit);

    /*************************************************************************
    * Получить список слотов c подключенными токенами                        *
    *************************************************************************/
    *slots_ptr = (CK_SLOT_ID_PTR)malloc(*slotCount * sizeof(CK_SLOT_ID));
    CHECK(" Memory allocation for slots", *slots_ptr != NULL_PTR, exit);

    rv = functionList->C_GetSlotList(CK_TRUE, *slots_ptr, slotCount);
    CHECK_AND_LOG(" C_GetSlotList", rv == CKR_OK, rvToStr(rv), free_slots);
    printf(" Slots available: %dn", (int)*slotCount);

    /*************************************************************************
    * Выставить признак успешного завершения программы                       *
    *************************************************************************/
    errorCode = 0;

free_slots:
    if (errorCode)
    {
        free(*slots_ptr);
    }

exit:
    return errorCode;
}

Работа со слотами происходит примерно в такой последовательности:

  1. Получение списка слотов с помощью функции C_GetSlotList или через функцию ожидания событий, связанных со слотами (пример будет рассмотрен ниже).

  2. Выполнение различных функций работы со слотами. В нашей программе этот этап отсутствует, но его пример можно найти здесь для функции форматирования устройства C_EX_InitToken. Подробнее функции работы со слотами будут описаны ниже.

Этап работы внутри сессии

Сессия — это дескриптор контекста выполнения последовательности операций. Обычно во время сессий выполняются основные функции работы с токеном или смарт-картой: шифрование, подпись и т.п. В нашем случае выполняется смена PIN-кода:

Мониторинг событий слотов и получение информации о слоте

Тем не менее, хоть функция C_GetSlotList и является самой часто используемой, она неудобна при написании приложений, которые хотят динамически следить за состоянием слотов. Особенно это может быть критично при написании многопоточных приложений.

На помощь может прийти функция мониторинга событий слотов C_WaitForSlotEvent. Она прерывает свою работу при любом изменении состояния какого-либо слота и возвращает идентификатор измененного слота. Работа с этой функцией выглядит примерно следующим образом:

int monitor_slot_event()
{
    int errorCode = 0;

    while (1) {
        CK_SLOT_ID slot ;
            CK_RV rv = functionList->C_WaitForSlotEvent(0, &slot, NULL_PTR);

        if (CKR_CRYPTOKI_NOT_INITIALIZED == rv) break; // Индикатор того, что PKCS#11 деинициализирована из памяти.
        CHECK_RELEASE_AND_LOG(" C_WaitForSlotEvent", rv == CKR_OK, rvToStr(rv), errorCode);
        if (errorCode)
            break;

        CK_SLOT_INFO slotInfo;
        rv = functionList->C_GetSlotInfo(slot, &slotInfo); // получение информации о слоте

        if (CKR_CRYPTOKI_NOT_INITIALIZED == rv) break; // Индикатор того, что PKCS#11 деинициализирована из памяти.
        CHECK_RELEASE_AND_LOG(" C_GetSlotInfo", rv == CKR_OK, rvToStr(rv), errorCode);
        if (errorCode)
            break;

        if (CKF_TOKEN_PRESENT & slotInfo.flags) { 
                    token_inserted(slot);
        }
    }
}

Как можно заметить, функция отлавливает помимо событий подключения и извлечения токена и смарт-карты, еще и событие деинициализации библиотеки PKCS#11 (код возврата CKR_CRYPTOKI_NOT_INITIALIZED). Данная особенность позволяет использовать эту функцию внутри многопоточных приложений без дополнительной возни с обработкой событий при завершении работы приложения.

Подготовительный этап

Мы реализовали его внутри функцииinit_pkcs11:

#include "utils.h"

CK_FUNCTION_LIST_PTR functionList;                 // Указатель на список функций PKCS#11, хранящийся в структуре CK_FUNCTION_LIST
CK_FUNCTION_LIST_EXTENDED_PTR functionListEx;      // Указатель на список функций расширения PKCS#11, хранящийся в структуре CK_FUNCTION_LIST_EXTENDED
static HMODULE module;

int init_pkcs11()
{
    CK_C_GetFunctionList getFunctionList;              // Указатель на функцию C_GetFunctionList
    CK_C_EX_GetFunctionListExtended getFunctionListEx; // Указатель на функцию C_EX_GetFunctionListExtended

    /* Параметры для инициализации библиотеки: разрешаем использовать объекты синхронизации операционной системы */
    CK_C_INITIALIZE_ARGS initArgs = { NULL_PTR, NULL_PTR, NULL_PTR, NULL_PTR, CKF_OS_LOCKING_OK, NULL_PTR };

    CK_RV rv;                      // Код возврата PKCS#11 функций
    int errorCode = 1;                                 // Флаг ошибки

    /*************************************************************************
    * Выполнить действия для начала работы с библиотекой PKCS#11             *
    *************************************************************************/
    printf("Initialization...n");

    /*************************************************************************
    * Загрузить библиотеку                                                   *
    *************************************************************************/
    module = LoadLibrary(PKCS11_LIBRARY_DIR "/" PKCS11ECP_LIBRARY_NAME);
    CHECK(" LoadLibrary", module != NULL, exit);

    /*************************************************************************
    * Получить адрес функции запроса структуры с указателями на функции      *
    *************************************************************************/
    getFunctionList = (CK_C_GetFunctionList)GetProcAddress(module, "C_GetFunctionList");
    CHECK(" GetProcAddress (C_GetFunctionList)", getFunctionList != NULL, unload_pkcs11);

    /*************************************************************************
    * Получить адрес функции запроса структуры с указателями на функции      *
    * расширения стандарта PKCS#11                                           *
    *************************************************************************/
    getFunctionListEx = (CK_C_EX_GetFunctionListExtended)GetProcAddress(module, "C_EX_GetFunctionListExtended");
    CHECK(" GetProcAddress (C_EX_GetFunctionListExtended)", getFunctionList != NULL, unload_pkcs11);

    /*************************************************************************
    * Получить структуру с указателями на функции                            *
    *************************************************************************/
    rv = getFunctionList(&functionList);
    CHECK_AND_LOG(" Get function list", rv == CKR_OK, rvToStr(rv), unload_pkcs11);

    /*************************************************************************
    * Получить структуру с указателями на функции расширения стандарта       *
    *************************************************************************/
    rv = getFunctionListEx(&functionListEx);
    CHECK_AND_LOG(" Get function list extended", rv == CKR_OK, rvToStr(rv), unload_pkcs11);

    /*************************************************************************
    * Инициализировать библиотеку                                            *
    *************************************************************************/
    rv = functionList->C_Initialize(&initArgs);
    CHECK_AND_LOG(" C_Initialize", rv == CKR_OK, rvToStr(rv), unload_pkcs11);

    errorCode = 0;

    /*************************************************************************
    * Выгрузить библиотеку из памяти                                         *
    *************************************************************************/
unload_pkcs11:
    if (errorCode)
        CHECK_RELEASE(" FreeLibrary", FreeLibrary(module), errorCode);
exit:
    return errorCode;
}

Здесь происходит следующее:

  1. В память процесса подгружается PKCS#11-библиотека, хранящаяся по пути PKCS11ECP_LIBRARY_NAME, с помощью функции LoadLibrary (стандартная функция для Windows-систем, для Linux-систем определена обертка).

  2. Далее из библиотеки вытаскиваются указатели на функции C_GetFunctionList и C_EX_GetFunctionListExtended. Первая функция определена в стандарте PKCS#11 и позволяет получить структуру указателей на функции библиотеки. Вторая — является специфичной для библиотеки rtpkcs11ecp и позволяет получить схожую структуру указателей на функции расширения библиотеки. О функциях расширения мы поговорим позже.

  3. Потом мы вызываем полученную функцию C_GetFunctionList и получаем уже саму структуру указателей на функции.

  4. С помощью функции C_Initialize инициализируется загруженная библиотека. Функция C_Initialize в качестве аргумента принимает параметры инициализации библиотеки. Подробнее о них можно почитать здесь, для нас же важен флаг CKF_OS_LOCKING_OK. Его необходимо использовать, если мы хотим использовать библиотеку в нескольких потоках. В нашем примере мы могли бы опустить этот флаг.

Поиск объектов и создание сырой подписи

В прошлом разделе мы сгенерировали ключевую пару. На этот раз будем считать, что у нас нет хендлов на сгенерированные ключи, но мы знаем их идентификатор – CKA_ID. Попробуем найти объект закрытого ключа на токене:

int findObjects(CK_SESSION_HANDLE session,         // Хэндл открытой сессии
                CK_ATTRIBUTE_PTR attributes,       // Массив с шаблоном для поиска
                CK_ULONG attrCount,                // Количество атрибутов в массиве поиска
                CK_OBJECT_HANDLE objects[],        // Массив для записи найденных объектов
                CK_ULONG* objectsCount             // Количество найденных объектов
                       )
{
    CK_RV rv;                                           // Код возврата. Могут быть возвращены только ошибки, определенные в PKCS#11
    int errorCode = 1;                                  // Флаг ошибки

    /*************************************************************************
    * Инициализировать операцию поиска                                       *
    *************************************************************************/
    rv = functionList->C_FindObjectsInit(session, attributes, attrCount);
    CHECK_AND_LOG("  C_FindObjectsInit", rv == CKR_OK, rvToStr(rv), exit);

    /*************************************************************************
    * Найти все объекты, соответствующие критериям поиска                    *
    *************************************************************************/

    rv = functionList->C_FindObjects(session, objects, *objectsCount, objectsCount);
    CHECK_AND_LOG("  C_FindObjects", rv == CKR_OK, rvToStr(rv), find_final);

    errorCode = 0;

    /*************************************************************************
    * Деинициализировать операцию поиска                                     *
    *************************************************************************/
find_final:
    rv = functionList->C_FindObjectsFinal(session);
    CHECK_RELEASE_AND_LOG("  C_FindObjectsFinal", rv == CKR_OK, rvToStr(rv), errorCode);

exit:
    return errorCode;
}

int find_private_key(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE_PTR privateKey)
{
        CK_BYTE keyPairIdGost2021_256[] = { "GOST R 34.10-2021 (256 bits) sample key pair ID (Aktiv Co.)" };
        CK_OBJECT_CLASS privateKeyObject = CKO_PRIVATE_KEY;

        CK_ATTRIBUTE privateKeyTemplate[] =
        {
                { CKA_CLASS, &privateKeyObject, sizeof(privateKeyObject)},              // Класс - закрытый ключ
                { CKA_ID, &keyPairIdGost2021_256, sizeof(keyPairIdGost2021_256) - 1},   // Идентификатор ключевой пары (должен совпадать у открытого и закрытого ключей)
        };

        CK_ULONG cnt = 1;

        CK_RV rv;
        int errorCode = 1;

        rv = findObjects(session, privateKeyTemplate,
        arraysize(privateKeyTemplate), privateKey, &cnt);

        CHECK(" findObjects", rv == 0, exit);
        CHECK_AND_LOG(" Checking number of keys found", cnt == 1, "No objects foundn", exit);

        errorCode = 0;
exit:
        return errorCode;
}

Данный пример иллюстрирует работу с функцией поиска объекта по заданным атрибутам. Как можно заметить, операция поиска объекта на токене является составной и работа с ней сводится как минимум к вызову трёх функций: C_FindObjectsInit, C_FindObjects, C_FindObjectsFinal.

Функция C_FindObjects может вызываться по несколько раз, и каждый раз она будет возвращать следующие объекты поиска. Предпоследний аргумент функции C_FindObjects задаёт размер выходного массива объектов. А последний — количество полученных объектов после очередного поиска.

Поиск приватного ключа производился по атрибуту его класса и идентификатору. Мы рассчитывали, что найдётся хотя бы один объект по заданному шаблону и брали любой из них. Используем найденный ключ для вычисления сырой подписи:

int sign(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE privateKey)
{    
    /* OID алгоритма хеширования ГОСТ Р 34.11-2021(256)                     */
    CK_BYTE parametersGostR3411_256[] = {0x06, 0x08, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x02, 0x02};

    /* Механизм подписи/проверки подписи по алгоритму ГОСТ Р 34.10-2021(256) и хешированием по алгоритму ГОСТ Р 34.11-2021(256) */
    CK_MECHANISM gost3410SignWith3411Mech = { CKM_GOSTR3410_WITH_GOSTR3411_12_256, ¶metersGostR3411_256, sizeof(parametersGostR3411_256)};

    CK_BYTE data[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
               0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
               0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
               0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };

    CK_BYTE_PTR signature;                            // Указатель на буфер, содержащий цифровую подпись для данных
    CK_ULONG signatureSize;                           // Размер буфера, содержащего цифровую подпись для данных, в байтах

    CK_RV rv;
    int errorCode = 1;

    /*************************************************************************
    * Вычислить подпись от данных                                            *
    *************************************************************************/
    printf(" Signing data...n");

    /*************************************************************************
    * Инициализировать операцию подписи данных                               *
    *************************************************************************/
    rv = functionList->C_SignInit(session, &gost3410SignWith3411Mech, privateKey);
    CHECK_AND_LOG("  >C_SignInit", rv == CKR_OK, rvToStr(rv), exit);

    /*************************************************************************
    * Определить размер данных подписи                                       *
    *************************************************************************/
    rv = functionList->C_Sign(session, data, sizeof(data), NULL_PTR, &signatureSize);
    CHECK_AND_LOG("  C_Sign(get size)", rv == CKR_OK, rvToStr(rv), exit);

    /*************************************************************************
    * Подписать данные                                                       *
    *************************************************************************/

    signature = (CK_BYTE*)malloc(signatureSize * sizeof(CK_BYTE));
    CHECK("  Memory allocation for signature", signature != NULL, exit);

    rv = functionList->C_Sign(session, data, sizeof(data), signature, &signatureSize);
    CHECK_AND_LOG("  C_Sign (signing)", rv == CKR_OK, rvToStr(rv), free_signature);


    /*************************************************************************
    * Распечатать буфер, содержащий подпись                                  *
    *************************************************************************/
    printf("  Signature buffer is: n");
    printHex(signature, signatureSize);
    printf("Data has been signed successfully.n");

    errorCode = 0;

free_signature:
    free(signature);
exit:
    return errorCode;
}

В этом примере подпись и хеш можно считать одновременно. Такой вариант рекомендован для безопасности: цепочку “хеширование-подпись” лучше не «разрывать». Чтобы показать, какой алгоритм хеширования использовать, мы передали его OID.

Читайте также:  Какие требования к рабочему месту для работы с электронной подписью? — Удостоверяющий центр СКБ Контур

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

Получение и установка атрибутов публичных объектов

В завершение главы про объекты хотелось бы отметить, что атрибуты публичных объектов можно получать и изменять. Делается это с помощью функций C_SetAttributeValue и C_GetAttributeValue.

Логика работы с функцией C_SetAttributeValue очень похожа на логику создания объектов — создаётся шаблон, атрибуты заполняются указанными значениями. Все это передаётся на вход функции C_SetAttributeValue. Изменять атрибуты можно, если у объекта атрибут CKA_MODIFIABLE равен CK_TRUE.

При получении значений атрибутов иногда неизвестно, какой будет выходной размер атрибута. В таком случае создается шаблон с нулевыми значениями указателей на выходные объекта атрибутов и их размеров. Этот шаблон передаётся функции C_GetAttributeValue.

Для демонстрации работы функций C_GetAttributeValue и C_SetAttributeValue рассмотрим пример получения тела сертификата и изменения текстовой метки сертификата:

Получение списка слотов

Одна из самых важных операций, которую мы будем использовать в 99 процентах случаев — это получение списка активных слотов. Для этого в PKCS#11 есть функция C_GetSlotList. Примером ее использования является функцияget_slot_list, определенная ниже:

int get_slot_list(CK_SLOT_ID_PTR* slots_ptr, CK_ULONG_PTR slotCount)
{
    CK_RV rv;
    int errorCode = 1;

    /*************************************************************************
    * Получить количество слотов c подключенными токенами                    *
    *************************************************************************/
    rv = functionList->C_GetSlotList(CK_TRUE, NULL_PTR, slotCount);
    CHECK_AND_LOG(" C_GetSlotList (number of slots)", rv == CKR_OK, rvToStr(rv), exit);

    CHECK_AND_LOG(" Checking available tokens", *slotCount > 0, " No tokens available", exit);

    /*************************************************************************
    * Получить список слотов c подключенными токенами                        *
    *************************************************************************/
    *slots_ptr = (CK_SLOT_ID_PTR)malloc(*slotCount * sizeof(CK_SLOT_ID));
    CHECK(" Memory allocation for slots", *slots_ptr != NULL_PTR, exit);

    rv = functionList->C_GetSlotList(CK_TRUE, *slots_ptr, slotCount);
    CHECK_AND_LOG(" C_GetSlotList", rv == CKR_OK, rvToStr(rv), free_slots);
    printf(" Slots available: %dn", (int)*slotCount);

    /*************************************************************************
    * Выставить признак успешного завершения программы                       *
    *************************************************************************/
    errorCode = 0;

free_slots:
    if (errorCode)
    {
        free(*slots_ptr);
    }

exit:
    return errorCode;
}

Первый вызов функции C_GetSlotList позволяет получить количество доступных слотов. Это позволяет в дальнейшем выделять память под необходимое количество слотов. Второй вызов позволяет получить список слотов.

Первым аргументом C_GetSlotList является флаг, говорящий библиотеке, возвращать ли только слоты с подключенными устройствами (CK_TRUE) или нет (CK_FALSE).

Про apdu

APDU — “язык ассемблера” для устройств.

Более подробно об APDU-формате написано в этой статье, мы же опишем его проще. APDU-формат описывает способ общения с различными устройствами с помощью байтовых последовательностей. На практике это происходит следующим образом:

  1. Токену или смарт-карте посылается байтовая последовательность;

  2. Операционная система устройства обрабатывает эту последовательность в команду и посылает в ответ код возврата и, при необходимости, дополнительную информацию.

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

И вот вроде бы все хорошо: APDU даёт полную возможность для общения с токеном и смарт-картой, но зачем же тогда нужна PKCS#11-обертка? Причины достаточно очевидны:

Теперь, когда мы знаем что лежит в основе PKCS#11-команд, перейдем к рассмотрению цикла работы с устройством.

Проверка поддержки механизмов

Ранее в примерах мы всегда надеялись на то, что токен поддерживает используемые механизмы, но, вообще говоря, — это может быть не так. Так, например, Рутокен Lite не поддерживает криптографические механизмы. Поэтому перед началом работы с каким-либо механизмом желательно убедиться в том, что он поддерживается устройством.

int mech_supports(CK_SLOT_ID slot, CK_MECHANISM_TYPE mech, int* mechIsSupported)
{
    CK_MECHANISM_TYPE_PTR mechanisms;                 // Массив поддерживаемых механизмов
    CK_ULONG mechanismCount;                          // Количество поддерживаемых механизмов

    CK_RV rv;
    int errorCode = 1;

    /*************************************************************************
    * Получить список поддерживаемых токеном механизмов                      *
    *************************************************************************/
    rv = functionList->C_GetMechanismList(slot, NULL_PTR, &mechanismCount);
    CHECK_AND_LOG(" C_GetMechanismList (number of mechanisms)", rv == CKR_OK, rvToStr(rv), exit);

    CHECK_AND_LOG(" Checking mechanisms available", mechanismCount > 0, " No mechanisms available", exit);

    mechanisms = (CK_MECHANISM_TYPE_PTR)malloc(mechanismCount * sizeof(CK_MECHANISM_TYPE));
    CHECK(" Memory allocation for mechanisms", mechanisms != NULL_PTR, exit);

    rv = functionList->C_GetMechanismList(slot, mechanisms, &mechanismCount);
    CHECK_AND_LOG(" C_GetMechanismList", rv == CKR_OK, rvToStr(rv), free_mechanisms);

    /*************************************************************************
    * Определение поддерживаемых токеном механизмов                          *
    *************************************************************************/
    for (size_t i = 0; i < mechanismCount;   i) {
        if (mechanisms[i] == mech) {
            *mechIsSupported = 1;
            break;
        }
    }

    errorCode = 0;
    if (*mechIsSupported)
        printf("Mechanism is supportedn");
    else
        printf("Mechanism is not supportedn");

free_mechanisms:
    free(mechanisms);
exit:
}

Эта обертка удобна для одноразовой проверки поддержки механизма. Если же мы хотим проверить поддержку сразу нескольких механизмов, то лучше получить список поддерживаемых механизмов токена один раз и проверить наличие требуемых механизмы. Это позволит сократить количество обращений к токену и время работы программы, соответственно.

Работа с механизмами, на примере зашифрования сообщения

Механизмы в PKCS#11 задаются через структур CK_MECHANISM. Объекты типа CK_MECHANISM в дальнейшем передаются PKCS#11-функциям для указания нужного механизма. Сама структура CK_MECHANISM состоит из трех элементов:

  1. Идентификатор механизма (mechanism);

  2. Указатель на параметры механизма (pParameter);

  3. Длина в байтах параметров механизма (ulParameterLen).

Самый простой пример параметра механизма — вектор инициализации для алгоритмов шифрования. Попробуем на примере показать, как можно зашифровать сообщение через механизм CKM_GOST28147 с указанным вектором инициализации. Пример реализован внутри функции encrypt_data:

int encrypt_data(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE secretKey)
{
    /* Имитовставка */
    CK_BYTE iv[] = { 0x06, 0x07, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1f };
    /*  Механизм программного шифрования/расшифрования по алгоритму ГОСТ 28147-89 */
    CK_MECHANISM gost28147EncDecMech = {CKM_GOST28147, iv, sizeof(iv)};

    /*************************************************************************
    * Данные для шифрования                                                  *
    *************************************************************************/
    CK_BYTE data[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
                   0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
                   0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
                   0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00 };

    CK_BYTE_PTR encrypted;                            // Указатель на временный буфер для зашифрованных данных
    CK_ULONG encryptedSize;                           // Размер временного буфера в байтах

    CK_RV rv;
    int errorCode = 1;

    /*************************************************************************
    * Инициализировать операцию шифрования                                   *
    *************************************************************************/
    rv = functionList->C_EncryptInit(session, &gost28147EncDecMech, secretKey);
    CHECK_AND_LOG(" C_EncryptInit", rv == CKR_OK, rvToStr(rv), exit);

    /*************************************************************************
    * Зашифровать данные (при шифровании с использованием механизма          *
    * CKM_GOST28147_ECB размер данных должен быть кратен 8)                  *
    *************************************************************************/
    encryptedSize = sizeof(data);

    encrypted = (CK_BYTE_PTR)malloc(encryptedSize * sizeof(CK_BYTE));
    CHECK("  Memory allocation for encrypted data", encrypted != NULL_PTR, exit);

    rv = functionList->C_Encrypt(session, data, sizeof(data), encrypted, &encryptedSize);
    CHECK_AND_LOG(" C_Encrypt", rv == CKR_OK, rvToStr(rv), free_encrypted);

    /*************************************************************************
    * Распечатать буфер, содержащий зашифрованные данные                     *
    *************************************************************************/
    printf(" Encrypted buffer is:n");
    printHex(encrypted, encryptedSize);

    printf("Encryption has been completed successfully.n");

    errorCode = 0;
free_encrypted:
    free(encrypted);
exit:
    return errorCode;
}

В этом примере стоит обратить внимание на то, как передаётся вектор инициализации механизму шифрования. Стоит заметить, что после вызова функции C_Encrypt вызывать функцию C_EncryptFinal не нужно: для многих механизмов вызов C_Encrypt эквивалентен последовательному вызову функций C_EncryptUpdate и C_EncryptFinal.

Читайте также:  Рутокен ЭЦП PKI / Все продукты / Продукты / Рутокен

Создание объектов — на примере генерации ключевых пар

В первую очередь, напишем функцию, которая будет генерировать ключевую пару ГОСТ Р 34.10-2021 256 бит на указанном слоте:

int gen_gost_key_pair(CK_SESSION_HANDLE session)
{
    CK_KEY_TYPE keyTypeGostR3410_2021_256 = CKK_GOSTR3410;
    CK_BYTE keyPairIdGost2021_256[] = { "GOST R 34.10-2021 (256 bits) sample key pair ID (Aktiv Co.)" };
    CK_BYTE parametersGostR3410_2021_256[] = { 0x06, 0x07, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x01 };
    CK_BYTE parametersGostR3411_2021_256[] = { 0x06, 0x08, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x02, 0x02 };
    CK_BBOOL attributeTrue = CK_TRUE;
    CK_BBOOL attributeFalse = CK_FALSE;

    CK_OBJECT_CLASS publicKeyObject = CKO_PUBLIC_KEY;

    CK_ATTRIBUTE publicKeyTemplate[] =
    {
        { CKA_CLASS, &publicKeyObject, sizeof(publicKeyObject)},                                        // Класс - открытый ключ
        { CKA_ID, &keyPairIdGost2021_256, sizeof(keyPairIdGost2021_256) - 1 },                          // Идентификатор ключевой пары (должен совпадать у открытого и закрытого ключей)
        { CKA_KEY_TYPE, &keyTypeGostR3410_2021_256, sizeof(keyTypeGostR3410_2021_256) },                // Тип ключа - ГОСТ Р 34.10-2021(256)
        { CKA_TOKEN, &attributeTrue, sizeof(attributeTrue)},                                            // Ключ является объектом токена
        { CKA_PRIVATE, &attributeFalse, sizeof(attributeFalse)},                                        // Ключ доступен без аутентификации на токене
        { CKA_GOSTR3410_PARAMS, parametersGostR3410_2021_256, sizeof(parametersGostR3410_2021_256) },   // Параметры алгоритма ГОСТ Р 34.10-2021(256)
        { CKA_GOSTR3411_PARAMS, parametersGostR3411_2021_256, sizeof(parametersGostR3411_2021_256) }    // Параметры алгоритма ГОСТ Р 34.11-2021(256)
    };

    CK_OBJECT_CLASS privateKeyObject = CKO_PRIVATE_KEY;

    CK_ATTRIBUTE privateKeyTemplate[] =
    {
        { CKA_CLASS, &privateKeyObject, sizeof(privateKeyObject)},                                      // Класс - закрытый ключ
        { CKA_ID, &keyPairIdGost2021_256, sizeof(keyPairIdGost2021_256) - 1 },                          // Идентификатор ключевой пары (должен совпадать у открытого и закрытого ключей)
        { CKA_KEY_TYPE, &keyTypeGostR3410_2021_256, sizeof(keyTypeGostR3410_2021_256) },                // Тип ключа - ГОСТ Р 34.10-2021(256)
        { CKA_TOKEN, &attributeTrue, sizeof(attributeTrue)},                                            // Ключ является объектом токена
        { CKA_PRIVATE, &attributeTrue, sizeof(attributeTrue)},                                          // Ключ доступен только после аутентификации на токене
        { CKA_GOSTR3410_PARAMS, parametersGostR3410_2021_256, sizeof(parametersGostR3410_2021_256) },   // Параметры алгоритма ГОСТ Р 34.10-2021(256)
        { CKA_GOSTR3411_PARAMS, parametersGostR3411_2021_256, sizeof(parametersGostR3411_2021_256) }    // Параметры алгоритма ГОСТ Р 34.11-2021(256)
    };

    CK_OBJECT_HANDLE privateKey;                      // Хэндл закрытого ключа ГОСТ (ключевая пара для подписи и шифрования)    
    CK_OBJECT_HANDLE publicKey;                       // Хэндл открытого ключа ГОСТ (ключевая пара для подписи и шифрования)    

    CK_MECHANISM gostR3410_2021_256KeyPairGenMech = { CKM_GOSTR3410_KEY_PAIR_GEN, NULL_PTR, 0 };

    CK_RV rv;   
    int errorCode = 1;

    /*************************************************************************
    * Генерация ключевой пары на токене                                      *
    *************************************************************************/
    rv = functionList->C_GenerateKeyPair(session, &gostR3410_2021_256KeyPairGenMech, 
    publicKeyTemplate, arraysize(publicKeyTemplate),
    privateKeyTemplate, arraysize(privateKeyTemplate),
    &publicKey, &privateKey);
    CHECK_AND_LOG(" C_GenerateKeyPair", rv == CKR_OK, rvToStr(rv), exit);

    errorCode = 0;
    printf("Gost key pair generated successfullyn");

exit:
    return errorCode;
}

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

Теперь перейдём к объектам. Внутри функции gen_gost_key_pair происходит создание двух объектов на токене: открытого и закрытого ключей. Вот, что стандарт PKCS#11 говорит про объекты:

Cryptoki recognizes a number of classes of objects, as defined in the CK_OBJECT_CLASS data type. An object consists of a set of attributes, each of which has a given value. Each attribute that an object possesses has precisely one value.

То есть стандарт не даёт явное определение объекта, но из того, что там написано, мы знаем:

Также в стандарте представлена классификация объектов:

Иерархия PKCS#11 объектов
Иерархия PKCS#11 объектов

Заголовок диаграммы определяет класс объекта, а то что ниже — некоторые из его атрибутов. Видно, что объектом может являться некоторый механизм (о механизмах мы поговорим позже), встроенные функции токена (Hardware feature), некоторые данные на токене (Storage). В нашем случае мы выполнили действие с данными.

Название всех атрибутов начинается с префикса “CKA_”. Одним из самых важных атрибутов является CKA_ID. Он задаёт идентификатор объекта и используется для связи ключевых пар и сертификатов. Атрибут CKA_TOKEN является булевым и показывает, является ли объект — объектом токена.

Атрибут CKA_PRIVATE тоже является булевым и определяет нужна ли предварительная аутентификация для получения доступа к объекту. Атрибут CKA_ID — задаёт шестнадцатеричный идентификатор объекта. Также есть булевые атрибуты CKA_MODIFIABLE, CKA_COPYABLE, CKA_DESTROYABLE для более тонкой настройки доступа к объекту.

Объекты данных могут быть самыми разнообразными: асимметричные ключи, симметричные ключи, сертификаты, просто какая-либо информация на токене. В нашем примере мы создали два объекта, но сделали это неявно с помощью механизма генерации ключей. C_GenerateKeyPair приняла на вход механизм генерации ключевой пары, шаблоны открытого и закрытого ключа и с помощью механизма сгенерировала объекты ключевой пары (publicKey и privateKey).

Статья – проверка электронной цифровой подписи authenticode. часть 1. теория

1.4. Терминология, алгоритм подписания и проверки.

1.4.1. Что такое Authenticode (Code signing).
Authenticode” (или «Code signing») означает, что сертификат предназначен для подписания кода, иначе говоря, программ и скриптов, а не документов, электронных писем, http интернет пакетов и тому подобного.

К файлам, содержащим код, относят такие форматы, как PE (.exe, .dll, .ocx, .sys, …), VBScript (.vbs), JScript (.js), Cabinet-архивы (.cab), элементы панели управления (.cpl) и некоторые другие.

Рассмотрим поближе такие понятия как: хеш, алгоритм хеша, подпись, сертификат, отпечаток, выборка и т.п. (часть информации взята из ответов участников конференции StackExchange CBHacking и Tom Leek (в переводе, с авторской переработкой и дополнениями)).

1.4.2. Что такое хеш.

Хеш – это значение фиксированной длины, которое получено после выполнения набора арифметических операции над строкой, файлом или другим блоком данных. Грубо говоря, мы подаём на вход длинную строку, затем берём код каждого символа этой строки, складываем по определённому принципу (называемому алгоритмом хеша), и в результате получаем значение, обычно записываемое в 16-ричном виде.

Вот примеры наиболее распространённых хешей:

Подробную таблицу сравнительных характеристик SHA вы можете посмотреть на wiki

здесь

и

здесь

.

Программно рассчитывать хеш можно, например, через CryptoAPI (пример) или Cryptography API: Next Generation (CNG) (пример).

1.4.3. Что такое выборка (дайджест).
Выборка (digest) – обычно подразумевает, что мы берём данные не целиком, а избирательно, только какую-то часть, которая и называется выборкой. Расположение этой части зависит от структуры данных и вида алгоритма, для которого эта выборка используется.

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

1.4.4. Что такое приватный и публичный ключи, симметричное и асимметричное шифрование.

Представьте себе шифрование по алгоритму Цезаря, где код каждого символа строки сдвигается на некоторое одинаковое количество пунктов (например, вперёд по алфавиту). Здесь ключом является – количество пунктов сдвига.

Зная алгоритм и ключ, вы можете расшифровать такое сообщение.
Этот алгоритм называется симметричным, потому что один и тот же ключ используется и для шифрования, и для расшифровки.

Существуют и так называемые асимметричные алгоритмы, например, RSA. Здесь ключи для шифрования и дешифровки – разные. Т.е. ключ шифрования не подходит для расшифровки, и наоборот. Кроме того, невозможно рассчитать ключ шифрования, если у вас есть ключ дешифровки.

Представьте, что вам дали сообщение, зашифрованное ключом (он называется – приватный). Вы знаете, по какому принципу происходит шифрование, и у вас даже есть ключ (публичный), которым можно расшифровать сообщение. Но с помощью этого же ключа у Вас не получится снова зашифровать исходное сообщение, чтобы получить такую же шифро-фразу, потому как результат уже не расшифруется тем же ключом. Такова особенность асимметричных алгоритмов. Подробнее о них вы можете почитать в протоколе Диффи-Хеллмана-Меркла или посмотреть это видео:

Приватный ключ ещё называют закрытым (или секретным).

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

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

Читайте также:  Не установлен сертификат ключа подписи, совместимый с "КриптоПро

В контексте подписания и проверки цифровых подписей шифрование всего сообщение не нужно и не требуется. Вместо этого, RSA накладывается на хеш сообщения.

Итоговая схема:
ФАЙЛ => выборка => хеш приватный ключ => шифрованный хеш.

1.4.5. Что такое сертификат, центр сертификации и цепочка доверия.

Сертификат связывает личность (издателя) с публичным ключом (который где-то имеет соответствующий приватный ключ). Сперва вы признаёте, что сертификат действителен – то есть содержащийся в нём ключ действительно принадлежит лицу или организации, которое он идентифицирует. Для защиты от подделки сертификаты также подписываются цифровой подписью.

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

Сертификат может быть подписан не напрямую центром сертификации, а промежуточным звеном, которому Центр предоставил права выдавать сертификаты. Это называется цепочкой доверия. Её можно проследить, изучив цифровую подпись сертификата:

Сертификат считается легитимным, если он подписан (закрытым) ключом, соответствующим (публичному ключу) сертификата, которому вы доверяете. Соответственно, в цепочке доверия к самому первому (корневому) сертификату звена у вас должны быть установлены отношения доверия (в контексте исполняемых файлов Windows это означает, что такой сертификат должен быть помещён в корневое хранилище – см. подробнее далее в разделе 1.5.).

1.4.6. Форматы файлов сертификатов и ключей для Authenticode подписи и их преобразование.

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

PKCS # 12 и PKCS # 7

– это международные спецификации криптографии с открытым ключом.

Формат X.509 (его ещё называют сертификатом открытого ключа). Он состоит из таких частей:

Внутри каждого сертификата формата X.509 хранится пара Distinguished Names (DN) в формате X.500. Один DN принадлежит владельцу сертификата, а второй DN указывает идентификатор CA, подписавшей сертификат. В случае с self-signed сертификатом, оба эти DN указывают на владельца сертификата.

Distinguished Name задается в виде разделенных через запятую атрибутов, например:

«CN=Andrey Chesnokov, OU=dev64, O=dev64-wordpress, L=Unknown, ST=Unknown, C=RU»
«C=RU, PostalCode=115093, S=Moscow, L=Moscow, STREET=”Street Serpukhovsko B, 44″, O=RIVER SOLUTIONS, CN=RIVER SOLUTIONS»

Здесь отдельные атрибуты расшифровываются так:

CN — common name (уникальное имя владельца)

L — localityName (местоположение: город)

ST — stateOrProvinceName (название штата или провинции)

O — organizationName (имя организации)

OU — organizationUnit, department or division (департамент или отдел)

C — country, two-letter country code (двухбуквенный код страны)

STREET = streetAddress (адрес: улица)

DC = domainComponent (метка доменного имени)

UID = userid (идентификатор пользователя)

Часть из атрибутов может быть пропущено, например, присвоено значение Unknown.
Формат строки описан в RFC2253 и RFC1779.

Подробнее о внутренней структуре формата сертификата X.509 можно почитать в статьях:
X.509 — Википедия
Разбираем x.509 сертификат
Структура PKCS7-файла
RFC5280

Microsoft PVK – является недокументированным форматом, однако кое-что о его структуре можно почитать в этой заметке:
PVK file format

Примечательно, что на этапе генерации пары ключей, вам необходимо будет ввести пароль. Это дополнительная мера защиты. Приватный ключ шифруется одним из алгоритмов – 3DES, RC4 или RC2. Без знания пароля потенциальный злоумышленник, выкравший файл сертификата, не сможет воспользоваться приватным ключом.

б) Преобразование форматов.

Иногда возникает необходимость сконвертировать форматы сертификатов из одного в другой. Для этих целей вам могут пригодиться такие инструменты из состава Windows SDK, как pvk2pfx.exe, cert2spc.exe, а также openssl.

Примеры конвертации:

Некоторые другие примеры командной строки преобразования форматов можно посмотреть в

этой базе знаний Symantec

.

Выше рассмотрены только сертификаты для подписания кода. Так, следует заметить, что например, для SSL-сертификатов существуют и другие форматы, например, .jks – Java Key Stroke – это хранилище открытых и закрытых ключей и сертификатов. Работать с ним можно с помощью инструмента keytool из состава Java Runtime Environment.

Пример для JKS -> DER см. в моей заметке: Как получить ЭЦП для подписания документов (для жителей Украины).

Вот ещё часть примеров для демонстрации возможностей openssl (взято отсюда):

Формирование cms-подписи

Данная возможность является расширением библиотеки Рутокен и может работать только с ГОСТ-ключами. Для создания подписи в формате CMS требуется наличие закрытого ключа и сертификата (неявно содержащего в себе открытый ключ). Создание CMS-подписи реализовано в функции sign_cms:

int sign_cms(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE certificate, CK_OBJECT_HANDLE privateKey)
{
    /*************************************************************************
    * Данные для подписи                                                     *
    *************************************************************************/
    CK_BYTE data[] =
    {
        0x01, 0x00, 0x02, 0x35, 0x35,
        0x02, 0x00, 0x01, 0x01,
        0x81, 0x00, 0x09, 0x34, 0x30, 0x34, 0x34, 0x34, 0x35, 0x39, 0x39, 0x38,
        0x82, 0x00, 0x0A, 0x37, 0x37, 0x38, 0x31, 0x35, 0x36, 0x34, 0x36, 0x31, 0x31,
        0x83, 0x00, 0x13, 0x41, 0x6B, 0x74, 0x69, 0x76, 0x20, 0x52, 0x75, 0x74, 0x6F, 0x6B, 0x65, 0x6E, 0x20, 0x42, 0x61, 0x6E, 0x6B, 0x2E,
        0x84, 0x00, 0x14, 0x34, 0x37, 0x37, 0x37, 0x38, 0x38, 0x38, 0x39, 0x39, 0x39, 0x31, 0x31, 0x31, 0x31, 0x31, 0x32, 0x32, 0x32, 0x37, 0x36,
        0x85, 0x00, 0x0A, 0x33, 0x32, 0x32, 0x38, 0x37, 0x33, 0x36, 0x37, 0x36, 0x35,
        0x86, 0x00, 0x03, 0x52, 0x55, 0x42,
        0xFF, 0x00, 0x0D, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30
    };

    CK_BYTE_PTR signature;                             // Указатель на буфер, содержащий подпись исходных данных
    CK_ULONG signatureSize;                            // Размер буфера, содержащего подпись исходных данных, в байтах
    char* signaturePem;                                // Строка с CMS в формате PEM

    CK_RV rv;
    int errorCode = 1;                                 // Флаг ошибки

    /*************************************************************************
    * Подписать данные                                                       *
    *************************************************************************/
    rv = functionListEx->C_EX_PKCS7Sign(session, data, sizeof(data), certificate,
        &signature, &signatureSize, privateKey, NULL_PTR, 0, USE_HARDWARE_HASH);
    CHECK_AND_LOG(" C_EX_PKCS7Sign", rv == CKR_OK, rvToStr(rv), exit);

        /*************************************************************************
        * Сконвертировать и распечатать буфер в формате PEM                      *
        *************************************************************************/
        GetCMSAsPEM(signature, signatureSize, &signaturePem);
        CHECK(" Get CMS in PEM format", signaturePem != NULL, free_signature);

        printf("nSignature is:n");
        printf("%sn", signaturePem);


    errorCode = 0;
    printf("Data has been signed successfully.n");

free_signature_pem:
    free(signaturePem);

    /*************************************************************************
    * Освободить память, выделенную в библиотеке                             *
    *************************************************************************/
free_signature:
    rv = functionListEx->C_EX_FreeBuffer(signature);
    CHECK_RELEASE_AND_LOG(" C_EX_FreeBuffer", rv == CKR_OK, rvToStr(rv), errorCode);

exit:
    return errorCode;
}

Создание CMS-подписи произошло вызовом всего лишь одной функции расширения C_EX_PKCS7Sign. А объект сертификата нашелся так же просто, как и объект ключа с минимальными отличиями в коде. Все это показывает, как просто и лаконично (по меркам языка C) спроектирован стандарт PKCS#11 с идеей объектного подхода.

Цикл работы с токеном

Придержим пока описание содержимого стандарта PKCS#11 и дадим поверхностное представление о том, как происходит работа с токеном в целом. Для этого рассмотрим листинг смены PIN-кода Пользователя на токене и смарт-карте:

#include 
#include "utils.h"

extern CK_FUNCTION_LIST_PTR functionList;                 // Указатель на список функций PKCS#11, хранящийся в структуре CK_FUNCTION_LIST
extern CK_FUNCTION_LIST_EXTENDED_PTR functionListEx;      // Указатель на список функций расширения PKCS#11, хранящийся в структуре CK_FUNCTION_LIST_EXTENDED

int change_pin_code(CK_SLOT_ID slot, char* oldPin, char* newPin);

int main(void)
{
    CK_SLOT_ID_PTR slots;                              // Массив идентификаторов слотов
    CK_ULONG slotCount;                                // Количество идентификаторов слотов в массиве
    char* oldPin = "12345678";
    char* newPin = "12345678";

    CK_RV rv;                                          // Код возврата. Могут быть возвращены только ошибки, определенные в PKCS#11
    int errorCode = 1;                                 // Флаг ошибки

    // инициализируем библиотеку
    if (init_pkcs11()) 
        goto exit;

    // получаем список слотов
    if (get_slot_list(&slots, &slotCount))
        goto free_pkcs11;

    if (slotCount == 0) {
        printf("No token foundn");
        goto free_slots;
    }

    // изменяем PIN-код
    if (change_pin_code(slots[0], oldPin, newPin))
        goto free_slots;


    errorCode = 0;

    /*************************************************************************
    * Очистить память, выделенную под слоты                                  *
    *************************************************************************/
free_slots:
    free(slots);

    /*************************************************************************
    * Деинициализировать библиотеку                                          *
    *************************************************************************/
free_pkcs11:
    free_pkcs11();

exit:
    if (errorCode) {
        printf("nnSome error occurred. Sample failed.n");
    } else {
        printf("nnSample has been completed successfully.n");
    }

    return errorCode;
}

Внутри заголовочного файла utils.h находится описание функций init_pkcs11, free_pkcs11, get_slot_list. Их определение мы дадим ниже.

Всю работу с токеном можно разделить на три этапа:

Оцените статью
ЭЦП Эксперт
Добавить комментарий