Русский
Русский
English
Статистика
Реклама

Блог компании «актив»

PKCS11 для самых маленьких

03.03.2021 08:18:42 | Автор: admin

В этой статье мы познакомимся со стандартом PKCS#11, предназначенным для работы с различными криптографическими устройствами. Для демонстрации мы будем использовать токены и смарт-карты Рутокен ЭЦП 2.0.

Чтобы вам было проще понять эту информацию, перед её прочтением желательно:

Исходя из определения из Википедии:

PKCS #11 один из стандартов семейства Public-Key Cryptography Standards (PKCS). Он определяет платформонезависимый программный интерфейс доступа к криптографическим устройствам смарт-картам, токенам, криптографическим ускорителям, серверам ключей и другим средствам криптографической защиты информации. Иногда именуется Cryptoki.

Проще говоря, PKCS#11 определяет платформонезависимый набор функций, структуры, константы и т.п. для работы с криптографическими устройствами. Эти функции могут быть реализованы внутри различных библиотек, например, opensc-pkcs11 или в нашей библиотеке по работе с устройствами Рутокен rtpkcs11ecp. Библиотеки могут отличаться не только реализацией, но и самим набором функций, типов и констант. Это возможно, так как стандарт PKCS#11 описывает различные способы расширения, что позволяет добавлять свои функции, например, для работы с CMS-подписью, флеш-памятью и т.п.

Но давайте обо всем по порядку. В первую очередь, определимся, что из себя представляют функции PKCS#11-библиотек. Функции PKCS#11 внутри это обертки для работы с токенами и смарт-картами через APDU команды

Про APDU

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

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

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

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

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

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

  • Работать с шестнадцатеричными байтовыми последовательностями весьма неудобно, каждый раз надо заглядывать в спецификацию команд. Хотелось бы иметь их текстовый эквивалент, а лучше целую библиотеку для разработки.

  • Программирование через APDU-команды имеет низкий КПД. При выполнении даже самой простой операции надо написать множество команд. PKCS#11 устраняет эту проблему: при выполнении одной функции PKCS#11 посылается несколько APDU команд. Меньше пишешь больше делаешь.

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

Репозиторий с примерами

Чтобы улучшить понимание представленных ниже примеров рекомендуем:

  1. Клонировать репозиторий с примерами.

  2. Выполнить настройку системы согласно README.

  3. Попробовать собрать примеры.

  4. Отформатировать устройство семейства Рутокен ЭЦП 2.0 с помощью примера ex0_format_token.

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

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

#include #include "utils.h"extern CK_FUNCTION_LIST_PTR functionList;                 // Указатель на список функций PKCS#11, хранящийся в структуре CK_FUNCTION_LISTextern CK_FUNCTION_LIST_EXTENDED_PTR functionListEx;      // Указатель на список функций расширения PKCS#11, хранящийся в структуре CK_FUNCTION_LIST_EXTENDEDint 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 found\n");        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("\n\nSome error occurred. Sample failed.\n");    } else {        printf("\n\nSample has been completed successfully.\n");    }    return errorCode;}

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

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

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

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

#include "utils.h"CK_FUNCTION_LIST_PTR functionList;                 // Указатель на список функций PKCS#11, хранящийся в структуре CK_FUNCTION_LISTCK_FUNCTION_LIST_EXTENDED_PTR functionListEx;      // Указатель на список функций расширения PKCS#11, хранящийся в структуре CK_FUNCTION_LIST_EXTENDEDstatic 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. Его необходимо использовать, если мы хотим использовать библиотеку в нескольких потоках. В нашем примере мы могли бы опустить этот флаг.

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

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

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

Слот это дескриптор виртуального интерфейса, куда подключен токен. Конкретно в нашем примере весь этап работы со слотами уместился в определение одной функции 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: %d\n", (int)*slotCount);    /*************************************************************************    * Выставить признак успешного завершения программы                       *    *************************************************************************/    errorCode = 0;free_slots:    if (errorCode)    {        free(*slots_ptr);    }exit:    return errorCode;}

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

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

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

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

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

int change_pin_code(CK_SLOT_ID slot, char* oldPin, char* newPin){    CK_SESSION_HANDLE session;                         // Хэндл открытой сессии    CK_RV rv;                                          // Код возврата. Могут быть возвращены только ошибки, определенные в PKCS#11    int errorCode = 1;    /*************************************************************************    * Открыть RW сессию в первом доступном слоте                             *    *************************************************************************/    rv = functionList->C_OpenSession(slot, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL_PTR, NULL_PTR, &session);    CHECK_AND_LOG(" C_OpenSession", rv == CKR_OK, rvToStr(rv), exit);    /*************************************************************************    * Выполнить аутентификацию Пользователя                                *    *************************************************************************/    rv = functionList->C_Login(session, CKU_USER, oldPin, strlen(oldPin));    CHECK_AND_LOG(" C_Login (CKU_USER)", rv == CKR_OK, rvToStr(rv), close_session);    /*************************************************************************    * Установить PIN-код Пользователя по умолчанию                           *    *************************************************************************/    printf("\nChanging user PIN to default...\n");    rv = functionList->C_SetPIN(session, NULL_PTR, 0, newPin, strlen(newPin));    CHECK_AND_LOG(" C_SetPIN", rv == CKR_OK, rvToStr(rv), logout);    printf("User PIN has been changed to default successfully.\n");    errorCode = 0;    /*************************************************************************    * Сбросить права доступа                                                 *    *************************************************************************/logout:    rv = functionList->C_Logout(session);    CHECK_RELEASE_AND_LOG(" C_Logout", rv == CKR_OK, rvToStr(rv), errorCode);    /*************************************************************************    * Закрыть открытую сессию в слоте                                        *    *************************************************************************/close_session:    rv = functionList->C_CloseSession(session);    CHECK_RELEASE_AND_LOG(" C_CloseSession", rv == CKR_OK, rvToStr(rv), errorCode);exit:    return errorCode;}

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

  1. Открытие сессии в слоте с помощью функции C_OpenSession.

  2. Аутентификация пользователя с помощью функции C_Login. Стандартно на Рутокене присутствуют два пользователя: Администратор (CKU_SO) и Пользователь (CKU_USER). Аутентификация на устройстве не является обязательной операцией. Она нужна, когда мы хотим получить доступ к операциям и объектам, которые требуют наличия соответствующих прав доступа. В нашем случае, это операция смены PIN-кода Пользователя.

  3. Выполнение различных функций по работе с сессиями. В нашем случае это функция C_SetPIN.

  4. Далее по нисходящей могут идти операции сброса прав доступа (C_Logout) и завершения сессии (C_CloseSession).

Завершающий этап

Весь наш завершающий этап поместился внутри определения функции free_pkcs11:

int free_pkcs11(){        CK_RV rv;        int errorCode;        printf("\nFinalizing... \n");    rv = functionList->C_Finalize(NULL_PTR);        CHECK_RELEASE_AND_LOG(" C_Finalize", rv == CKR_OK, rvToStr(rv), errorCode);    CHECK_RELEASE(" FreeLibrary", FreeLibrary(module), errorCode);        return errorCode;}

Завершающий этап можно разделить на:

  1. Деинициализацию библиотеки с помощью функции C_Finalize.

  2. Выгрузку библиотеки из памяти процесса через функцию FreeLibrary (для Linux-систем имеется обертка).

Классификация функций PKCS#11

Стандартные функции и функции расширения

В общем виде все функции внутри PKCS#11-библиотек можно разделить на:

  • Стандартные те, что явно описаны в стандарте. Список указателей на эти функции можно получить с помощью функции C_GetFunctionList, хранящейся в этой же библиотеке. Сами же стандартные функции имеют префикс "C_".

  • Расширения те, что были добавлены разработчиками библиотеки и не описаны в стандарте. Стандарт явно не определяет функцию для получения списка расширенных функций. Но в библиотеке rtpkcs11ecp этой функцией является C_EX_GetFunctionListExtended. Сами же функции-расширения обычно имеют префикс "C_EX_".

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

Разделение по предназначению

С другой стороны, функции PKCS#11 можно классифицировать по их предназначению. Логично выделить следующие виды функций:

  • Функции общего назначения. Например, для получения списка функций или получения информации о библиотеке.

  • Функции работы со слотами. Они не зависят от контекста работы с токеном. Например, C_GetSlotInfo, C_GetTokenInfo и т.п.

  • Функции для работы с сессиями. Как уже говорилось ранее, обычно во время выполнения этих функций осуществляется основное взаимодействие с токеном. При выполнении функции внутри сессии контекст работы с токеном не теряется. Самый простой пример этих функций: функция аутентификации (C_Login), функции работы с закрытым ключом на токене (C_Sign, C_Encrypt) и т.п.

Атомарные и составные операции

Некоторые операции в PKCS#11 работают привычным для нас образом: вызвал функцию получил результат. Но есть и операции, реализация которых выстроена через многократный вызов функций. Ярким примером такой операции является блочное шифрование: чтобы зашифровать данные нужно вызвать функции: C_EncryptInit, C_EncryptUpdate, C_EncryptFinish. Пример этого будет приведен ниже. Эта особенность связана с тем, что внутри библиотеки PKCS#11 скрыт вызов APDU-команд, который как раз предусматривает разбиение некоторых команд на несколько итераций. Как уже говорилось ранее, это способствует эффективному использованию PKCS#11 с потоками данных.

Работа со слотами

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

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

Одна из самых важных операций, которую мы будем использовать в 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: %d\n", (int)*slotCount);    /*************************************************************************    * Выставить признак успешного завершения программы                       *    *************************************************************************/    errorCode = 0;free_slots:    if (errorCode)    {        free(*slots_ptr);    }exit:    return errorCode;}

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

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

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

Тем не менее, хоть функция 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). Данная особенность позволяет использовать эту функцию внутри многопоточных приложений без дополнительной возни с обработкой событий при завершении работы приложения.

Первым аргументом в функцию C_WaitForSlotEvent передается флаг блокировки (CKF_DONT_BLOCK). Если он установлен, то функция не является блокирующей. В таком случае, если никакой слот не был изменен, то возвращается код CKR_NO_EVENT.

Также стоит обратить внимание на использование функции получения информации о слоте C_GetSlotInfo. Мы использовали ее для определения наличия токена по флагу CKF_TOKEN_PRESENT. Подробнее с этой структурой можно ознакомиться здесь.

В этом файле представлен пример получения информации о подключенных токенах или смарт-картах с помощью функции C_GetTokenInfo. С помощью этой функции можно получить серийный номер, метку и другие характеристики подключенного устройства.

Объекты

Всё есть объект это почти про PKCS#11.

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

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

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

int gen_gost_key_pair(CK_SESSION_HANDLE session){    CK_KEY_TYPE keyTypeGostR3410_2012_256 = CKK_GOSTR3410;    CK_BYTE keyPairIdGost2012_256[] = { "GOST R 34.10-2012 (256 bits) sample key pair ID (Aktiv Co.)" };    CK_BYTE parametersGostR3410_2012_256[] = { 0x06, 0x07, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x01 };    CK_BYTE parametersGostR3411_2012_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, &keyPairIdGost2012_256, sizeof(keyPairIdGost2012_256) - 1 },                          // Идентификатор ключевой пары (должен совпадать у открытого и закрытого ключей)        { CKA_KEY_TYPE, &keyTypeGostR3410_2012_256, sizeof(keyTypeGostR3410_2012_256) },                // Тип ключа - ГОСТ Р 34.10-2012(256)        { CKA_TOKEN, &attributeTrue, sizeof(attributeTrue)},                                            // Ключ является объектом токена        { CKA_PRIVATE, &attributeFalse, sizeof(attributeFalse)},                                        // Ключ доступен без аутентификации на токене        { CKA_GOSTR3410_PARAMS, parametersGostR3410_2012_256, sizeof(parametersGostR3410_2012_256) },   // Параметры алгоритма ГОСТ Р 34.10-2012(256)        { CKA_GOSTR3411_PARAMS, parametersGostR3411_2012_256, sizeof(parametersGostR3411_2012_256) }    // Параметры алгоритма ГОСТ Р 34.11-2012(256)    };    CK_OBJECT_CLASS privateKeyObject = CKO_PRIVATE_KEY;    CK_ATTRIBUTE privateKeyTemplate[] =    {        { CKA_CLASS, &privateKeyObject, sizeof(privateKeyObject)},                                      // Класс - закрытый ключ        { CKA_ID, &keyPairIdGost2012_256, sizeof(keyPairIdGost2012_256) - 1 },                          // Идентификатор ключевой пары (должен совпадать у открытого и закрытого ключей)        { CKA_KEY_TYPE, &keyTypeGostR3410_2012_256, sizeof(keyTypeGostR3410_2012_256) },                // Тип ключа - ГОСТ Р 34.10-2012(256)        { CKA_TOKEN, &attributeTrue, sizeof(attributeTrue)},                                            // Ключ является объектом токена        { CKA_PRIVATE, &attributeTrue, sizeof(attributeTrue)},                                          // Ключ доступен только после аутентификации на токене        { CKA_GOSTR3410_PARAMS, parametersGostR3410_2012_256, sizeof(parametersGostR3410_2012_256) },   // Параметры алгоритма ГОСТ Р 34.10-2012(256)        { CKA_GOSTR3411_PARAMS, parametersGostR3411_2012_256, sizeof(parametersGostR3411_2012_256) }    // Параметры алгоритма ГОСТ Р 34.11-2012(256)    };    CK_OBJECT_HANDLE privateKey;                      // Хэндл закрытого ключа ГОСТ (ключевая пара для подписи и шифрования)        CK_OBJECT_HANDLE publicKey;                       // Хэндл открытого ключа ГОСТ (ключевая пара для подписи и шифрования)        CK_MECHANISM gostR3410_2012_256KeyPairGenMech = { CKM_GOSTR3410_KEY_PAIR_GEN, NULL_PTR, 0 };    CK_RV rv;       int errorCode = 1;    /*************************************************************************    * Генерация ключевой пары на токене                                      *    *************************************************************************/    rv = functionList->C_GenerateKeyPair(session, &gostR3410_2012_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 successfully\n");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). Мы пока ещё не описывали механизмы, но, говоря простым языком, механизм это идентификатор операции, которая выполняет какую-то криптографическую функцию. В нашем случае это функция генерации объекта.

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

В прошлом разделе мы сгенерировали ключевую пару. На этот раз будем считать, что у нас нет хендлов на сгенерированные ключи, но мы знаем их идентификатор 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 keyPairIdGost2012_256[] = { "GOST R 34.10-2012 (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, &keyPairIdGost2012_256, sizeof(keyPairIdGost2012_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 found\n", 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-2012(256)                     */    CK_BYTE parametersGostR3411_256[] = {0x06, 0x08, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x02, 0x02};    /* Механизм подписи/проверки подписи по алгоритму ГОСТ Р 34.10-2012(256) и хешированием по алгоритму ГОСТ Р 34.11-2012(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.

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

Работа с функциями расширения на примере создания запроса на сертификат

Пришло время показать, как работать с функциями-расширениями. Сделаем это на примере библиотеки PKCS#11 от Рутокен и создадим запрос на сертификат для нашей ключевой пары. Генерация запроса на сертификат не описана в стандарте, поэтому сделаем это через функцию-расширение C_EX_CreateCSR.

int create_csr(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE publicKey, CK_OBJECT_HANDLE privateKey){    /*************************************************************************    * Запрос на получение сертификата                                        *    *************************************************************************/    /*************************************************************************    * Список полей DN (Distinguished Name)                                   *    *************************************************************************/    CK_CHAR_PTR dn[] = { (CK_CHAR_PTR)"CN",                   // Тип поля CN (Common Name)                     (CK_CHAR_PTR)"UTF8String:Иванов",        // Значение                     (CK_CHAR_PTR)"C",                        // C (Country)                     (CK_CHAR_PTR)"RU",                     (CK_CHAR_PTR)"2.5.4.5",                  // SN (Serial Number)                     (CK_CHAR_PTR)"12312312312",                     (CK_CHAR_PTR)"1.2.840.113549.1.9.1",     // E (E-mail)                     (CK_CHAR_PTR)"ivanov@mail.ru",                     (CK_CHAR_PTR)"ST",                       // ST (State or province)                     (CK_CHAR_PTR)"UTF8String:Москва",                     (CK_CHAR_PTR)"O",                        // O (Organization)                     (CK_CHAR_PTR)"CompanyName",                     (CK_CHAR_PTR)"OU",                       // OU (Organizational Unit)                     (CK_CHAR_PTR)"Devel",                     (CK_CHAR_PTR)"L",                        // L (Locality)                     (CK_CHAR_PTR)"Moscow", };    /*************************************************************************    * Список дополнительных полей                                            *    *************************************************************************/    CK_CHAR_PTR exts[] = {(CK_CHAR_PTR)"keyUsage",                                                        // Использование ключа                      (CK_CHAR_PTR)"digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment",                      (CK_CHAR_PTR)"extendedKeyUsage",                                                    // Дополнительное использование                      (CK_CHAR_PTR)"1.2.643.2.2.34.6,1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.4",                      (CK_CHAR_PTR)"2.5.29.17",                                                           // Дополнительное имя (пример с кодированием в виде DER)                      (CK_CHAR_PTR)"DER:30:0F:81:0D:65:78:61:6d:70:6c:65:40:79:61:2E:72:75",                      (CK_CHAR_PTR)"2.5.29.32",                                                           // Политики сертификата (кодирование в виде DER с пометкой "critical")                      (CK_CHAR_PTR)"critical,DER:30:0A:30:08:06:06:2A:85:03:64:71:01",                      (CK_CHAR_PTR)"1.2.643.100.111",                                                     // Средства электронной подписи владельца                      (CK_CHAR_PTR)"ASN1:UTF8String:СКЗИ \\\"Рутокен ЭЦП 2.0\\\"", };    CK_BYTE_PTR csr;                                   // Указатель на буфер, содержащий подписанный запрос на сертификат    CK_ULONG csrSize;                                  // Размер запроса на сертификат в байтах    char* csrPem;                                      // Строка с CSR в формате PEM    CK_RV rv;    int errorCode = 1;    /*************************************************************************    * Создать запрос на сертификат                                           *    *************************************************************************/    printf("\nCreating CSR...\n");    /*************************************************************************    * Создание запроса на сертификат                                         *    *************************************************************************/    rv = functionListEx->C_EX_CreateCSR(session, publicKey, dn, arraysize(dn), &csr, &csrSize, privateKey, NULL_PTR, 0, exts, arraysize(exts));    CHECK_AND_LOG(" C_EX_CreateCSR", rv == CKR_OK, rvToStr(rv), exit);    /*************************************************************************    * Сконвертировать и распечатать буфер в формате PEM                      *    *************************************************************************/    GetCSRAsPEM(csr, csrSize, &csrPem);    CHECK(" Get CSR in PEM format", csrPem != NULL, free_csr);    printf("\nCertificate request is:\n");    printf("%s\n", csrPem);    errorCode = 0;    printf("Creating CSR has been completed successfully.\n");free_csr_pem:    free(csrPem);free_csr:    rv = functionListEx->C_EX_FreeBuffer(csr);    CHECK_RELEASE_AND_LOG(" C_EX_FreeBuffer", rv == CKR_OK, rvToStr(rv), errorCode);exit:    return errorCode;}

Можно заметить, что работа с функциями расширения очень похожа на работу со стандартными функциями. Основное отличие лишь в том, что мы обращаемся к другому списку функций CK_FUNCTION_LIST_EXTENDED_PTR. Создание запроса на сертификат происходит в одну строчку функцией C_EX_CreateCSR и возвращает запрос в DER-формате. Также стоит обратить внимание, что память, выделенную внутри библиотеки, следует высвобождать с помощью функции C_EX_FreeBuffer.

По полученному запросу можно получить сертификат в Удостоверяющем центре. Например, воспользуемся тестовым УЦ КриптоПро для получения сертификата. Полученный сертификат необходимо скачать в DER-формате, сохранить в файле с именем "cert_2012-256.cer" и положить в директорию, из которой вы запускаете примеры. Полученный сертификат можно импортировать на токен.

Импорт сертификата на токен. Создание объекта вручную

Сертификаты на токене так же как и ключи являются объектами. Сертификат можно импортировать на токен, создав объект с нужными атрибутами. Для этого мы реализовали функцию: import_cert:

int import_cert(CK_SESSION_HANDLE session){    CK_OBJECT_CLASS certificateObject = CKO_CERTIFICATE;    CK_BYTE keyPairIdGost2012_256[] = { "GOST R 34.10-2012 (256 bits) sample key pair ID (Aktiv Co.)" };    CK_BBOOL attributeTrue = CK_TRUE;    CK_BBOOL attributeFalse = CK_FALSE;    CK_CERTIFICATE_TYPE certificateType = CKC_X_509;    CK_ULONG tokenUserCertificate = 1;    /*************************************************************************    * Шаблон для импорта сертификата                                         *    *************************************************************************/    CK_ATTRIBUTE certificateTemplate[] =    {        { CKA_VALUE, 0, 0 },                                                               // Значение сертификата (заполняется в процессе работы)        { CKA_CLASS, &certificateObject, sizeof(certificateObject) },                      // Класс - сертификат        { CKA_ID, &keyPairIdGost2012_256, sizeof(keyPairIdGost2012_256) - 1 },             // Идентификатор сертификата (совпадает с идентификатором соответствующего ключа)        { CKA_TOKEN, &attributeTrue, sizeof(attributeTrue) },                              // Сертификат является объектом токена        { CKA_PRIVATE, &attributeFalse, sizeof(attributeFalse) },                          // Сертификат доступен без аутентификации        { CKA_CERTIFICATE_TYPE, &certificateType, sizeof(certificateType) },               // Тип сертификата - X.509        { CKA_CERTIFICATE_CATEGORY, &tokenUserCertificate, sizeof(tokenUserCertificate) }, // Категория сертификата - пользовательский    };    FILE* certFile;                                   // Поток ввода сертификата    CK_BYTE_PTR certDer;                              // Массив с сертификатом в DER формате    CK_ULONG certSize;                                // Размер массива сертификата    CK_OBJECT_HANDLE certificate;                     // Хэндл сертификата    CK_RV rv;    int r;    int errorCode = 1;                                // Флаг ошибки    /*************************************************************************    * Открыть поточный ввод сертификата из файла                             *    *************************************************************************/    certFile = fopen("cert_2012-256.cer", "rb");    CHECK_AND_LOG(" fopen", certFile != NULL, "\"cert_2012-256.cer\" doesn't exist", exit);    /*************************************************************************    * Определить размер файла, содержащего сертификат                        *    *************************************************************************/    r = fseek(certFile, 0, SEEK_END);    CHECK(" fseek", r == 0, close_certFile);    certSize = ftell(certFile);    CHECK(" ftell", certSize > 0, close_certFile);    r = fseek(certFile, 0, SEEK_SET);    CHECK(" fseek", r == 0, close_certFile);    /*************************************************************************    * Выделить память для сертификата                                        *    *************************************************************************/    certDer = (CK_BYTE_PTR)malloc(certSize);    CHECK(" malloc", certDer != NULL, close_certFile);    /*************************************************************************    * Прочитать сертификат                                                   *    *************************************************************************/    r = (int)fread(certDer, 1, (int)certSize, certFile);    CHECK(" fread", r == (int)certSize, free_certificate);    /*************************************************************************    * Задать шаблон сертификата для импорта                                  *    *************************************************************************/    certificateTemplate[0].pValue = certDer;    certificateTemplate[0].ulValueLen = certSize;    /*************************************************************************    * Создать сертификат на токене                                         *    *************************************************************************/    rv = functionList->C_CreateObject(session, certificateTemplate, arraysize(certificateTemplate), &certificate);    CHECK_AND_LOG(" C_CreateObject", rv == CKR_OK, rvToStr(rv), free_certificate);    errorCode = 0;    printf("Certificate has been created successfully\n");    /*************************************************************************    * Очистить память из-под строки с сертификатом                           *    *************************************************************************/free_certificate:    free(certDer);    /*************************************************************************    * Закрыть поток ввода сертификата                                        *    *************************************************************************/close_certFile:    r = fclose(certFile);    CHECK_RELEASE(" fclose", r == 0, errorCode);exit:    return errorCode;}

На этот раз мы создали объект напрямую с помощью функции C_CreateObject. Для создания объекта сертификата мы аналогично передали шаблон с атрибутами объекта: тело сертификата, идентификатор, тип доступа, тип сертификата и т.п.

Функцию C_CreateObject можно использовать не только для создания сертификата на токене, но и для создания других объектов, например, публичных ключей (CKO_PUBLIC_KEY), закрытых ключей (CKO_PRIVATE_KEY), симметричных ключей (CKO_SECRET_KEY), обычных данных (CKO_DATA). Их значение будет также содержаться внутри атрибута CKA_VALUE.

Теперь, когда у нас на токене имеется и ключевая пара и сертификат, для закрепления материала рассмотрим операцию создания CMS-подписи.

Формирование 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("%s\n", 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 с идеей объектного подхода.

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

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

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

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

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

int get_cert(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE cert){    CK_BYTE_PTR body = NULL_PTR;    CK_ATTRIBUTE template[] = {        {CKA_VALUE, NULL_PTR, 0}    };    char* certPem;    CK_RV rv;    int errorCode=1;        /*************************************************************************        * Получение размера тела сертификата                                     *        *************************************************************************/    rv = functionList->C_GetAttributeValue(session, cert, template, arraysize(template));    CHECK_AND_LOG(" C_GetAttributeValue", rv == CKR_OK, rvToStr(rv), exit);    body = (CK_BYTE_PTR) malloc(template[0].ulValueLen);    template[0].pValue = body;        /*************************************************************************        * Получение тела сертификата в формате DER                               *        *************************************************************************/    rv = functionList->C_GetAttributeValue(session, cert, template, arraysize(template));    CHECK_AND_LOG(" C_GetAttributeValue", rv == CKR_OK, rvToStr(rv), free);        /*************************************************************************        * Сконвертировать и распечатать буфер в формате PEM                      *        *************************************************************************/        GetCertAsPem(body, template[0].ulValueLen, &certPem);        CHECK(" Get cert in PEM format", certPem != NULL, free);        printf("\nCertificate request is:\n");        printf("%s\n", certPem);    errorCode = 0;        printf("Getting cert body has been completed successfully.\n");free_cert_pem:        free(certPem);free:    free(body);exit:    return errorCode;}int set_cert_label(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE cert){    CK_UTF8CHAR label[] = {"GOST certificate"};    CK_ATTRIBUTE template[] = {        CKA_LABEL, label, sizeof(label)-1    };    CK_RV rv;    int errorCode = 1;        /*************************************************************************        * Установка метки сертификата                                            *        *************************************************************************/    rv = functionList->C_SetAttributeValue(session, cert, template, arraysize(template));    CHECK_AND_LOG(" C_SetAttributeValue", rv == CKR_OK, rvToStr(rv), exit);    errorCode = 0;exit:    return errorCode;}

Про механизмы

Мы уже ранее встречались с механизмами в примерах и дали краткое описание. Давайте теперь опишем их подробнее. Описание механизмов в PKCS#11 было вынесено в отдельный документ, с которым можно ознакомиться здесь. В этом документе написано:

A mechanism specifies precisely how a certain cryptographic process is to be performed. PKCS #11 implementations MAY use one of more mechanisms defined in this docuоment.

Отсюда следует, что механизмы:

  • Определяют некоторое криптографическое преобразование.

  • PCKS#11 может использовать механизмы, определенные в этом документе.

Более того, некоторые PKCS#11-библиотеки могут использовать и другие механизмы.

Согласно документации механизмы можно разделить на:

  • Механизмы шифрования и расшифрования (Encrypt & Decrypt);

  • Механизмы подписи и проверки подписи (Sign & Verify);

  • Механизм формирования хеша (Digest);

  • Механизм восстановления подписи по публичному ключу (Sign Recover & Verify Recovery);

  • Механизм генерации симметричных и асимметричных ключей (Gen. Key/Key Pair);

  • Экспорт и импорт ключей (Wrap & Unwrap);

  • Выработка общего ключа на основе асимметричных ключей (Derive).

Каждый механизм идентифицирует одну или несколько из этих функций. Так, например, ранее рассмотренный механизм CKM_GOSTR3410_KEY_PAIR_GEN предназначен для генерации ключей, а механизм CKM_AES_ECB может использоваться как для зашифрования/расшифрования, так и для свертки/развертки ключей.

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

Механизмы в 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_EncryptFinish не нужно, т.к. C_Encrypt внутри себя вызывает функцию C_EncryptUpdate, а после неё уже завершает процесс шифрования с помощью функции C_EncryptFinish.

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

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

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 supported\n");    else        printf("Mechanism is not supported\n");free_mechanisms:    free(mechanisms);exit:}

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

Утилита pkcs11-tool

Часто бывает необходимо просто и быстро выполнить какое-либо обращение к PKCS#11-библиотеке, не прибегая к написанию кода. На помощь может прийти утилита pkcs11-tool, которая распространяется в составе пакета (opensc)[https://github.com/OpenSC/OpenSC].

Утилита pkcs11-tool может гибко выполнять огромное количество стандартных PKCS#11-операций. Например:

  • Генерация ключевых пар:
    pkcs11-tool --module /usr/lib/librtpkcs11ecp.so --keypairgen --key-type GOSTR3410-2012-256:B --id 45 -l

  • Создание сырой подписи:
    pkcs11-tool --module /usr/lib/librtpkcs11ecp.so --sign --id 45 -l -i file.txt --mechanism GOSTR3410-WITH-GOSTR3411-12-256

  • Генерация псевдослучайных последовательностей:
    pkcs11-tool --module /usr/lib/librtpkcs11ecp.so --generate-random 16

  • Получение списка объектов на токене:
    pkcs11-tool --module /usr/lib/librtpkcs11ecp.so -O

  • Получение механизмов, поддерживаемых токеном:
    pkcs11-tool --module /usr/lib/librtpkcs11ecp.so -M

Список не является исчерпывающим, более подробно о полных возможностях утилиты можно прочитать в мануале.

Дополнительный материал

Если вы захотите узнать, как работать с более специфичными функциями, то большое количество примеров по работе с PKCS#11 можно найти в нашем SDK. Все примеры по работе с PKCS#11-библиотекой находятся в директории sdk\pkcs11\samples.

Выводы

Зачастую библиотека PKCS#11 является основным кирпичиком при написании приложений или других библиотек для работы со смарт-картами на других языках программирования. Поэтому знание основ работы с PKCS#11 на языке C может помочь разобраться с тем, как работать с обертками или как лучше реализовать архитектуру новой обертки.

Кроме PKCS#11 с объектами на смарт-картах/токенах можно работать через:

Если вы хотите узнать про какую-то реализацию подробнее, то напишите в комментариях и мы постараемся в ближайшее время выпустить новую статью с её разбором.

Подробнее..

Как заголовок и навигация в документации помогают минимизировать стресс пользователя

18.09.2020 16:04:09 | Автор: admin


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


Первый. Перед вами система и 5 минут на то, чтобы выполнить в ней необходимую процедуру. Первым делом вы ищете нужную кнопку или пункт меню, не находите и начинаете нервничать, ведь время идёт, а хочется быть эффективным в любой своей деятельности. Напряжение делает вас расфокусированным, в этом состоянии вы продолжаете искать и в какой-то момент понимаете, что не сможете сами разобраться. Обращаетесь в техподдержку: набираете номер, играет приятная музыка и отрешённый голос сообщает вам, что вы пятнадцатый в очереди, напряжение растёт. Через какое-то время отвечает сотрудник техподдержки, он объясняет вам, что надо делать. Вы успокаиваетесь и выполняете свою задачу.



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





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





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


Хочешь, чтобы никто не нашел, назови не думая


Название инструкции это не название статьи в интернете. Ему не надо привлекать внимание и вызывать какие-то эмоции. Пользователь по названию должен понять, какая процедура описана в инструкции. Инструкцию надо называть так, чтобы её было несложно найти в интернете. Давайте более подробно разберёмся с этим.


Название инструкции не должно вызывать эмоции

Я использую два вида названий:


  • название-проблема;
  • название-процесс.

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


Что такое название-процесс


Цель инструкции с таким названием объяснить какой-то процесс.


Пример ситуации: пользователю надо в чём-то разобраться, у него есть достаточное количество времени и более менее спокойное состояние. По факту, его задача понять как что-то работает. Для инструкции, которая будет решать такую задачу я использую название-процесс.


Если это руководство пользователя, то в нём таких процессов может быть много. В этом случае мы уже говорим не про название инструкции, а про название раздела руководства.


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


Пример названия-процесса: Работа с программой Рутокен Диск.


Ситуация, когда будут искать эту инструкцию: пользователь купил устройство Рутокен с программой Рутокен Диск и хочет разобраться, как сохранить файл в защищённой флеш-памяти.

Его поисковый запрос: как сохранить файл в защищённом разделе или как работать с Рутокен Диском.


Он открывает инструкцию, выбирает свою операционную систему и раздел с названием Работа с защищённым разделом. Такой поиск у него займёт несколько секунд. Дальше он выполнит все действия этой процедуры и поймёт процесс.





Ещё один пример названия-процесса: Работа с Рутокен через КриптоПро CSP в macOS.


Ситуация, когда будут искать эту инструкцию: пользователь купил Рутокен с сертификатом КриптоПро и ему надо, чтобы всё это заработало на макбуке.


Его поисковый запрос: Рутокен КриптоПро macOS.


Он открывает инструкцию и выполняет последовательно все шаги из неё.





Пример плохого названия инструкции: Инструкция по форматированию Рутокена и установке нового PIN-кода для клиентов, переходящих с системы ДБО.


Ситуация, когда будут искать эту инструкцию: пользователю необходимо отформатировать токен перед тем как использовать его в другой системе или ему просто надо сменить PIN-код.


Мне это название не нравится по трём причинам:


  • Оно слишком длинное.
  • В нём фигурирует две процедуры: форматирование токена и смена PIN-кода.
  • Название сформулировано не так, как его будет искать пользователь.

И получается, что его может найти не только пользователь, который хочет отформатировать токен, но и тот, кто хочет сменить PIN-код. Пользователь откроет эту инструкцию, начнёт форматировать токен, а это приведёт к тому, что удалятся все сертификаты, которые стоят денег. Он хотел просто сменить PIN-код, а это надо было сделать иначе. Также у пользователя мог быть изначально пустой токен, тогда форматировать его не надо. На этом примере мы видим, что плохое название инструкции может привести даже к финансовым потерям. Эта инструкция подошла бы только в том случае, если пользователю надо было отформатировать Рутокен.


Плохое название может привести к финансовым потерям

Сейчас по запросу как изменить PIN-код Рутокена пользователь найдёт нашу инструкцию.


Что такое название-проблема


Цель инструкции с таким названием помочь решить проблему.


Пользователь работал в системе и в ней что-то произошло. Например, отобразилось сообщение с каким-то предупреждением или ошибкой, и он не знает, как правильно поступить. Он оказывается в стрессовой ситуации, от того, что он будет делать дальше зависит результат. Для описания таких случаев я использую название-проблему.


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


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


Пример такого названия: Недоступна смарт-карта Рутокен.


Ситуация, когда будут искать эту инструкцию: пользователь подключает смарт-карту Рутокен, а она не работает в системе, и он хочет разобраться, почему она не работает и, что сделать, чтобы заработала.


Его поисковый запрос: токены или смарт-карты недоступны. Он описывает проблему так, как она сформулирована в окне с ошибкой.


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


Второй пример названия, но теперь уже раздела: Что делать, если PIN-код Пользователя заблокирован.


Ситуация, когда будут искать эту инструкцию: пользователь ввёл несколько раз неправильный PIN-код, на экране отобразилось сообщение о том, что он заблокирован.


Самая эмоциональная реакция может возникнуть, когда с этим сталкиваются первый раз. Чаще всего пользователю кажется, что всё, доступ к Рутокену уже не вернуть, но это не так.


Его поисковый запрос будет таким: что делать, если PIN-код Пользователя заблокирован.


Он откроет эту инструкцию, найдёт нужный раздел и разблокирует PIN-код.


Пример плохого названия инструкции: Рутокен вставлен, но красный диод не горит. Что делать?


Что мне не нравится в этом названии:


  • Оно слишком длинное, в нём указаны лишние подробности, а именно то, что Рутокен вставлен. За счёт этого можно было сократить название.
  • Я бы заменила слово диод, не каждый пользователь его знает.


Давайте сделаем общие выводы:


  1. Прежде чем придумывать название для инструкции необходимо понять для кого эта инструкция и в какой ситуации пользователь будет её искать.
  2. При составлении названий используйте слова из мира пользователей.
  3. Подумайте о том, какой запрос пользователь введёт, когда будет искать инструкцию.
  4. Перед публикацией инструкции ещё раз прочитайте название и подумайте, может быть вы указали какие-то лишние подробности.


Хочешь, чтобы никто не нашёл, забудь про навигацию


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


Содержание как карта


Главное требования к содержанию инструкции оно должно быть логичным.
Цель содержания, как и у любой карты ускорить процесс поиска. Помочь пользователю быстро найти нужный раздел. Да, кто-то скажет, что в инструкции всегда можно воспользоваться внутренним поиском, нажав Ctrl+F, но это не всегда удобно. Если страниц в инструкции много, то внутренний поиск может только запутать пользователя.


Содержание должно быть логичным




Здесь тоже не обойтись без примеров. Давайте посмотрим на навигацию в одной из моих инструкций.


Самый простой случай, когда описана программа, которая работает в одной операционной системе. В нашем случае это программа Рутокен Логон. Посмотрим на то, как устроена навигация в инструкции для неё.





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


Простая последовательность изложения может выглядеть так:



Установка -> Запуск -> Настройка -> Процесс работы -> Удаление

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


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


Ранее мы рассматривали инструкцию про Рутокен Диск, она как раз была с таким содержанием. В ней пользователь находит раздел со своей операционной системой и подраздел с необходимой процедурой.


Такая последовательность изложения выглядит так:



Выбор операционной системы -> Запуск -> Настройка -> Процесс работы -> Удаление

Здесь просто добавилось название операционной системы, их в одной инструкции может быть много.


Пользователь должен находить название нужного раздела меньше чем за полминуты, иначе можно сделать вывод, что содержание не работает и его можно смело удалить.


У пользователя полминуты на то, чтобы найти нужный раздел

Я ещё очень люблю делать навигацию внутри разделов, это для тех пользователей, которые автоматически пропускают содержание.


Вот пример такой инструкции. Она написана для пользователей Рутокен U2F.





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


Содержание может всё испортить


Есть случай, когда содержание может действительно всё испортить. Его проще всего описать так: если пользователь пропустит любой шаг, то ничего работать не будет. Дорожная карта нужна в том случае, если дорог много. Но когда дорога одна и надо двигаться по ней не пропуская ни одного пункта, карта не нужна. Пользователь просто выполняет все шаги один за другим. Да, у меня таких инструкций немало, и они чаще всего про настройку чего-либо. Чтобы минимизировать стресс пользователя, в начале такой инструкции я объясняю ему, что необходимо последовательно выполнить все шаги этой инструкции, тогда у него сформируется определённый настрой.





Последний пример в этой статье как раз будет про это. Инструкция о том, как удалить сертификат. В самом её начале я объясняю пользователю, что прежде чем удалить сертификат, надо определить криптопровайдера или формат сертификата и записать имя контейнера. Это важно, так как можно удалить не тот сертификат, а вернуть его будет уже невозможно.





Поэтому у пользователя в начале инструкции нет возможности сразу перейти к какому-то разделу. Он открывает инструкцию и на первой странице читает предупреждение и последовательность действий. Здесь моя роль предупредить его о возможных рисках. Дальше он конечно выбирает сам, как ему действовать.



Давайте сделаем общие выводы:


  1. При составлении содержания учитывайте логику взаимодействия пользователя с тем, что вы описываете.
  2. Если пользователь пропустил содержание, то ему может помочь навигация в разделе.
  3. Если пропускать шаги в инструкции нельзя, то навигация не нужна.


Казалось бы все эти выводы простые и логичные, но я часто вижу, что про это забывают. Давайте всё-таки осознаем всё, что написано в этой статье и начнём писать так, чтобы минимизировать стресс пользователя. Ведь тогда мы сможем изменить его состояние, а также улучшить глобальное отношение к пользовательской документации.

Подробнее..

Как мы в Активе пишем пользовательскую документацию. Почему это важно

14.07.2020 06:19:36 | Автор: admin

Что пользователь хочет видеть в пользовательской документации? Что его в ней раздражает? Эти вопросы задаёт себе каждый, кто пишет такую документацию, но далеко не каждый правильно отвечает на них. Совсем небольшой процент пользователей читает документацию. Давайте разберёмся, почему так и как сделать пользовательскую документацию эффективной и изменить отношение пользователей к ней.



Нам мешает негативный опыт


С такой ситуацией сталкивался почти каждый: вам надо быстро освоить какую-то ИТ-систему, она очень сложная, и в ней хранятся важные данные. Метод проб и ошибок здесь не подойдёт. Наверняка в такой момент вы вспомнили про руководство пользователя, открыли его и начали внимательно изучать. После 10 минут чтения вы ещё больше запутались, закрыли руководство и начали осваивать систему, используя при этом метод будь что будет. И хорошо, если при этом вы не удалили что-то очень важное и не получили результат, о котором будете жалеть, а ведь всякое бывает.


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


Ответ один хорошая пользовательская документация.


Документация может всё


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


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


Это будет цикл статей, в которых я расскажу вам про основные ошибки в технической документации, мы посчитаем, сколько они стоят компании. Я объясню поведение пользователя, обозначу, с какими трудностями и эмоциями он сталкивается, когда читает плохой документ. Покажу, как можно исправить ошибки с минимальными затратами. Мои статьи для всех, кто пишет документацию и хочет делать это более осознанно, с заботой о пользователе. Если вы хоть в чем-то будете со мной не согласны, то пишите свои мысли в комментариях. Мы вместе можем сделать эту статью максимально полезной.


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


В этой статье я расскажу вам о том, сколько в нашей компании стоит пользовательская документация, и из каких этапов у нас состоит процесс документирования. Lets go!


Сколько стоит пользовательская документация


Документация стоит денег, давайте разберёмся каких. Если в компании есть технический писатель, то стоимость документации не измеряется только его заработной платой. Расскажу, почему это так.


Рассмотрим на примере инструкции, которую можно написать за 20 часов. Технический писатель за это время напишет 25-страничный документ, в котором будет около 40 скриншотов.


Весь процесс производства инструкции в нашей компании состоит из следующих этапов:


1. Поставка задачи. Её создаёт и описывает менеджер проекта, руководитель проекта или руководитель отдела. На это нужно время, недостаточно просто написать Опиши процедуру. Здесь обязательно нужны детали. Например, здесь посчитаем 1 час рабочего времени менеджера проекта. На этапе постановки задачи менеджер проекта назначает консультанта и обозначает круг сотрудников компании, которые могут помочь и объяснят какие-то важные особенности системы. Получается, что цель этого этапа сформулировать задачу и назначить консультанта



2. Показ системы. Чаще всего здесь задействован системный аналитик, он демонстрирует систему техническому писателю и рассказывает, что и как работает. Прибавим 2 часа работы аналитика + 2 часа технического писателя. Здесь аналитик рассказывает, а технический писатель слушает и задаёт вопросы. Если система сложная, таких встреч может быть несколько. Цель этого этапа объяснить техническому писателю, как работает система.



3. Написание документа. Здесь главную роль играет технический писатель, но нельзя забывать про вопросы, которые у него могут возникать в процессе написания. Отвечать на них может аналитик, разработчик, менеджер проекта, тот, кто разбирается в продукте и умеет понятно объяснять. Таких вопросов может быть много и хорошо, если технический писатель оперативно получает на них ответы. Прибавим 20 часов работы технического писателя + 1 час работы аналитика + 1 час работы разработчика. Цель этого этапа создать первую версию инструкции.



4. Рецензирование. Чаще всего читает документ заказчик, в нашем случае менеджер проекта. На это тоже нужно время, оно зависит от количества страниц в документе и качества описаний. Прибавим 2 часа работы менеджера проекта. На этом этапе он пишет комментарии к документу. Цель этого этапа проверить документ.



5. Внесение правок. Реализует технический писатель, на этом этапе он тоже может задавать уточняющие вопросы коллегам. Техническому писателю здесь очень важно правильно понять комментарии к инструкции. Прибавим 2 часа работы технического писателя + 1 час работы аналитика. Цель этого этапа создать вторую версию документа.



6. Тестирование. На этом этапе подключается сотрудник отдела тестирования. Он реализует все описания процедур и оценивает насколько корректно они описаны. Прибавим здесь 2 часа работы тестировщика. Цель этого этапа проверить корректность описания процедур.



7. Повторное внесение правок. Технический писатель получает от тестировщика комментарии и вносит изменения в инструкцию. Прибавим 1 час работы технического писателя. Цель этого этапа создание третьей версии документа.



8. Вёрстка документа. Здесь может подключаться дизайнер. Если вёрстка не очень сложная, то технический писатель делает её сам. Прибавим 4 часа работы дизайнера. Цель этого этапа сверстать документ так, чтобы его было удобно использовать.



9. Публикация документа. Мы публикуем документ на своём сайте на специальной странице. Прибавим 20 минут работы администратора сайта.



Итого:


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


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


100 тысяч рублей или долларов


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


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


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


  • Как лучше всего назвать документ, чтобы пользователь его быстро нашёл?
  • Как помочь пользователю быстро находить нужный раздел в документе?
  • Как сверстать документ так, чтобы сформировать у пользователя хорошее мнение о нём и системе?

See you soon!

Подробнее..

Как мы снимаем видеоинструкции для решений Рутокен

20.01.2021 10:13:24 | Автор: admin

Мы решили продолжить наш цикл статей про разработку пользовательской документации, но на этот раз нам захотелось рассказать еще об одном способе создания инструкций для пользователей это съемка видеороликов. Мы расскажем о процессе их производства: от возникновения идеи до загрузки ролика на YouTube-канал. У нас нет студии и профессиональных актеров, мы все делаем сами и хотим показать вам, что это не так сложно. Здесь вы найдете: идеи, готовые алгоритмы и, быть может, вдохновитесь на съемку своей видеоинструкции.

Когда обычной инструкции не достаточно

Иногда уже в процессе написания обычной инструкции понимаешь, хорошо бы показать какие-то процессы в видео. Например, если лампочка на устройстве, котором мы описываем, мигает несколько раз или меняет цвет. Да, можно написать если лампочка устройства мигает три раза, а потом гаснет, то устройство не работает. Вроде как всё понятно, а если лампочка не просто мигает, а несколько раз меняет цвет, то такое уже будет сложнее объяснить при помощи текста.

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

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

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

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

Все начинается с идеи и оценки ее реализации

Идея пришла уже после написания инструкции, но далеко не все подобные идеи заканчиваются реализацией. Ведь производство ролика стоит дороже написания инструкции и необходимо было осознать, действительно ли рационально сейчас тратить на это ресурсы компании. И мы задали себе вопрос: кому кроме наших пользователей может пригодиться такая видеоинструкция? Ее глобальная цель привлечь внимание к новой смарт-карте, поэтому она точно пригодится нашим менеджерам, они смогут отправлять ролик потенциальным заказчикам. Ещё такой ролик или его фрагмент можно показывать на презентации устройства, вставить в пресс-релиз и добавить в новостную ленту в социальных сетях. А этого уже достаточно для того, чтобы запустить процесс его производства. Ну и не стоит забывать про очевидную цель загрузить новый ролик на YouTube-канал, чтобы наши подписчики про нас лишний раз вспомнили. У вас цели могут быть другие.

Кто нужен для производства видеоинструкции

Идея есть, теперь нам надо понять, все ли необходимые специалисты сейчас могут поучаствовать в производстве ролика. Давайте разбираться, кто нам нужен:

  • Руководитель проекта или продукта. Как вы уже догадались, того продукта, для которого мы будем снимать ролик.

Зачем он нам: сформулировать цель ролика, ответить на вопросы в процессе написания сценария, вычитать сценарий и оценить ролик.

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

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

  • Монтажер. У нас это тот, кто писал сценарий, он собирает весь ролик. Видеооператор может помочь ему выполнить какие-то сложные моменты. В этот раз они были: нам надо было показать одновременную работу смарт-карты в двух мобильных операционных системах.

  • Человек, записывающий закадровый голос. У нас это реальный сотрудник технической поддержки, его голос знаком нашим пользователям. Он записывает закадровый голос для ролика.

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

Маркетолог. Он смотрит финальную версию и загружает ее на YouTube-канал.

Вроде никого не забыли. Да, многое может делать один человек и для каких-то роликов достаточно только заказчика и исполнителя.

Перед тем как мы запустим процесс производства видеоролика, нам надо выяснить, все ли участники свободны и смогут в какой-то момент подключиться к процессу. Мы все выяснили и определили сроки.

Сколько времени нужно на производство ролика

Со сроками тоже все интересно. Если рассказывать про обычную инструкцию, то мы понимаем примерно сколько это займет времени, с видео сложнее. Оно может совсем не понравиться и какие-то моменты придется переснимать, а потом и заново монтировать. По опыту, на ролик продолжительностью 5 минут в среднем нужно 6-8 рабочих дней, это время на весь цикл производства. И надо понимать, что иногда этот срок растягивается в два и в три раза. А бывает, когда все сразу удачно складывается и требуется меньше времени.

Что нужно из оборудования

  • Компьютер. На нем производится запись необходимого материала с экрана, хранятся все файлы для видео и монтируется ролик.

  • Штатив. На него можно закрепить мобильный телефон для съемки живых кадров.

  • Мобильный телефон. На него, как вы уже догадались, можно снимать живые кадры. Когда есть возможность позвать в этот проект видеооператора, штатив и мобильный телефон не нужны, так как процесс съемки обеспечивает видеооператор.

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

  • Монтажная программа. В ней мы монтируем и собираем видео. У нас их две: для простого линейного монтажа мы используем программу Movavi, а для более сложного Adobe Premiere Pro. Первая очень простая и быстрая в освоении. Конечно необходимо, чтобы все используемое ПО было лицензированным.

  • Много памяти на компьютере и мощный процессор. Чтобы сохранять все файлы для ролика и быстро его собирать. Файлы для ролика занимают в разы больше места, чем обычная документация.

  • Программа для обработки звука. В ней мы обрабатываем звук. Можно установить специальную, а можно использовать возможности программы Adobe Premiere Pro.

  • Текстовый редактор. В нем мы пишем сценарий. Использовать можно любой, но лучше всего тот, в котором организована возможность совместной работы с документом. Например, можно использовать Microsoft Word 2016 или Google Docs.

  • Программа для записи видео с экрана. С ее помощью мы снимаем видео с экрана, если это необходимо. Мы используем ту же программу, в которой мы делаем скриншоты для обычной документации, это программа Snagit.

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

Вроде все, сами понимаете, что если нет штатива, то это не критично. А вот, например, без монтажной программы точно не обойтись. Все оборудование собрано, начинаем снимать? Нет, сначала пишем сценарий.

Обязательно ли писать сценарий

Ответ ДА. Сценарий нужен даже, если ваш ролик длится 20 секунд. Ролики без сценариев очень отличаются, в них нет нужных акцентов и логики. А если в таком ролике еще есть закадровый голос, то это вообще веселье. Что делает человек, который записывает закадровый голос: он просто комментирует все, что видит, а так быть не должно. Здесь мы можем услышать неоправданные паузы, слова-паразиты и т.п.

Сценарий выстраивает логику повествования, дает возможность понять: где сделать паузу, выделить что-то голосом, замедлить или ускорить какие-то процессы.

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

Например, если пользователю необходимо указать какие-то конкретные значения параметров, то надо это сказать, а сами значения продублировать в субтитрах в кадре.

А вот, если мы показываем, как мигает индикатор на устройстве, то это словами комментировать не нужно, в ролике все и так будет видно.

Когда первая версия сценария написана, она отправляется на проверку заказчику. Это обязательно надо сделать перед началом съемки, так как у исполнителя и заказчика могут быть очень разные представления о содержании ролика, и важно это выяснить. Здесь не нужно торопиться, важно подождать, пока заказчик посмотрим и выдаст свои комментарии, иногда ждать приходится долго.

Комментарии получены, все ошибки исправлены, сценарий готов. Что делать дальше? С чего начать съемку? Начнем с самого сложного со съемки реальных кадров. В нашем ролике это кадры со смарт-картой, компьютером и мобильными телефонами.

Как снимать реальные кадры или записывать закадровый голос

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

Совет 1. Записывайте звук отдельно. Если вы снимаете живые кадры и хотите, чтобы они сопровождались закадровым голосом, то пишите звук отдельно. Качество звука это очень важный параметр.

Мы записываем закадровый голос специальный диктофон или на диктофон мобильного телефона.

Для записи звука вам нужно найти тихое помещение, так как не все шумы можно убрать в процессе обработки. Располагайте диктофон в 20 сантиметрах от губ говорящего.

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

Совет 2. Обрабатывайте звук. Убирайте шумы и выравнивайте звук по громкости.

Совет 3. Думайте про свет. Плохо когда света мало, но также плохо когда его много. Не снимайте против источника света. Он должен быть либо перед объектом съемки, либо сбоку.

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

Совет 4. Выстраивайте картинку. Даже, если в кадре нет человека, желательно думать о картинке. Думать о том, как это будет выглядеть, пробуйте разные варианты. Ведь порой даже от того, как расположен компьютер в кадре зависит восприятие всего ролика. Если для вас важно, чтобы было видно происходящее на экране компьютера, то старайтесь избегать лишних бликов. Здесь тоже нужен осознанный подход.

Мы снимали для ролика кадры:

  • Общий план: компьютер, считыватель, смарт-карта и руки человека. Так мы показали, что нужно для подготовки смарт-карты к работе.

  • Крупный план: смарт-карта в ладони. Так мы показали, как выглядит смарт-карта.

  • Крупный план: смарт-карта, мобильное устройство и руки человека. Так мы показали, как работать со смарт-картой на мобильном устройстве.

После того, как мы отсняли реальные кадры, начинаем снимать видео с экрана.

Как снимать видео с экрана компьютера или мобильного устройства

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

Не забывайте в таких роликах делать паузы, ведь пользователь смотрит и повторяет действия, а в отстающих ему быть не хочется.

Есть моменты, которые можно ускорить. Например, вы показываете как заполняете стандартные поля (фамилия, имя, отчество), такое пользователь сможет сделать сам.

А есть моменты, которые можно замедлить. Например, если показываете, что пользователю необходимо проверить значения полей. Здесь вам надо дать пользователю время на это.

Не забывайте про то, что в вашем ролике не должно быть каких-то личных данных (номеров телефонов, IP-адресов, адресов электронных почт и т.п.), их надо замылить.

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

Мы снимали для нашего ролика процедуру создания тестового сертификата через сервис ra.rutoken.ru. Это нужно, чтобы подготовить смарт-карту для работы с мобильным устройством. Ну и показали пользователю продуктовую страницу смарт-карты.

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

Теперь у вас есть реальные кадры, есть кадры, снятые с экрана, что делать дальше? Монтировать ролик?

Нужна ли музыка

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

Мы используем одну и туже музыку во всех роликах, купили ее несколько лет назад на всем известном сайте.

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

Музыку выбрали, теперь можно монтировать.

Как монтировать ролик

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

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

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

Редактирование голоса. При редактирование голоса необходимо вырезать: паузы, ошибки, слова-паразиты.

Текстовые слайды и субтитры. В начале видео, на заставке надпись может быть крупной.

Во всех остальных видеофрагментах их лучше делать поменьше и размещать в нижней трети кадра.

Также стоит придерживаться единой палитры. Яркие цвета можно использовать для расстановки акцентов, для всего остального нейтральные. Для лучшего сочетания оттенков можно воспользоваться цветовым кругом.

Картинки. Иногда нужно иллюстрировать какие-то процессы, в этом могут помочь картинки. Как уже писали, для создания иллюстраций мы используем онлайн-редактор Readymag. Там есть готовые изображения, из которых можно составить смысловой слайд.

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

На монтаж тоже нужно время. У нас на это ушел один рабочий день. Потом мы собрали первую версию ролика и отправили на проверку заказчику. Он прислал нам комментарии, мы обсудили их и смонтировали финальную версию ролика. Ее отправили нашему маркетологу, на этом этапе может возникнуть необходимость что-то исправить. Нас попросили исправить заставку, мы это сделали, и потом ролик был опубликован на нашем YouTube-канале.

Жизнь ролика на YouTube

Имейте ввиду, что для ролика еще надо написать описание и создать значок. Давайте сначала расскажем про значок. Это картинка, которая будет отображаться, пока ваш ролик не будет запущен для просмотра. К ней есть некоторые требования. Вставим сюда скриншот со страницы помощи YouTube.

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

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

Также мы открыты к диалогу, если есть вопросы пишите в комментарии.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru