Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

Добрый день!
Есть задача:
На конечной машине (пользовательской) проверять подпись на файле с помощью импортированного самоподписанного сертификата. Важно что на конечной машине нужно пользоваться ТОЛЬКО pki-core. На моей машине (администраторской) можно пользоваться чем угодно. Обе машины на Astra Linux

Схема реализации:
1. На своей машине (администраторская) генерю ключевую пару:

openssl genpkey -out "keyfile.key" -algorithm gost2012_256 -pkeyopt paramset:A

2. Выпускаю самоподписанный сертификат (в формате DER, потому что pki-core дружит только с ним, поправьте если не прав)

openssl req -key "keyfile.key" -new -x509 -days 999 -subj '*SUBJ*' -outform DER -out "cert.der"

3. Конвертирую сертификат в PEM потому что для cms подписи OpenSSL нужен именно он (опять же поправьте если ошибаюсь)

openssl x509 -inform der -in cert.der -out cert.pem

4. Подписываю файл с помощью OpenSSL

openssl cms -sign -inkey "keyfile.key" -signer cert.pem -in message.txt -nosmimecap -nodetach -outform DER -out signed_message.txt -nocerts

5. Импортирую сертификат в формате DER в токен как доверенный

auto certData = readFile ("/path/to/cert.der");
device.importCert (rutoken::pkicore::ExternalCert (certData.data (), certData.size ()), true);

6. Пытаюсь верифицировать подпись на токене

auto signedData = rutoken::pkicore::cms::SignedData::parse (readFile ("/path/to/signed_message.txt"));
auto verifyParams = rutoken::pkicore::cms::VerifyParams (device);
auto result = signedData.verify (verifyParams);

При вызове метода verify выбрасывается исключение "Bad parameters"

Подскажите что я делаю не так и как с этим бороться

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

nicetas.matthias, добрый день!

Дело в том, что в CMS нет пользовательского сертификата (флаг "-nocerts"), и в VerifyParams он тоже не передан.
Поэтому необходим вызов:

verifyParams.addUserCert(rutoken::pkicore::ExternalCert (certData.data (), certData.size ()));

(2023-11-29 14:11:08 отредактировано nicetas.matthias)

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

Добрый день, Павел Анфимов, !
Я думал что сертификаты хранимые на устройстве по-умолчанию используются при проверке подписи.

Сделал как вы написали в двух вариантах

1. Добавил сертификаты из самого токена (что мне и нужно)

for (auto &iter : device.enumerateCerts ())
{
    auto cert = rutoken::pkicore::ExternalCert (iter.toPem ());
    verifyParams.addUserCert (cert);
}

2. Добавил сертификат, с которым подписывал через OpenSSL из файла

auto сertBuf = readFile ("/path/to/cert.pem");
rutoken::pkicore::ExternalCert cert (std::string (сertBuf.begin (), сertBuf.end ()));
verifyParams.addUserCert (cert);

И другим способом

auto certBuf = readFile ("/path/to/cert.der");
rutoken::pkicore::ExternalCert cert ((void*) certBuf.data (), certBuf.size());
verifyParams.addUserCert (cert);

Во всех случаях все также выбрасывается исключение "Bad parameters"

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

nicetas.matthias,

Продукт pki-core-cpp пока не поддерживают парамсеты ТК26 для ключей ГОСТ34.10-2012. Добавление поддержки планируется в начале будущего года.

Пока можно использовать другие парамсеты, которые также допускаются для ключей ГОСТ-2012.

Для этого:
0) Использовать rtengine: https://www.rutoken.ru/support/download/openssl/
1) Генерация ключа и самоподписанного сертификата:

./openssl genpkey -out "test/keyfile.key" -algorithm gost2012_256 -pkeyopt paramset:id-GostR3410-2001-CryptoPro-A-ParamSet -engine rtengine

./openssl req -key "test/keyfile.key" -new -x509 -days 999 -subj '/CN=test/' -outform DER -out "test/cert.der" -engine rtengine

./openssl x509 -inform der -in test/cert.der -out test/cert.pem -engine rtengine

2) подписание

./openssl cms -sign -inkey "test/keyfile.key" -signer test/cert.pem -in test/message.txt -nosmimecap -nodetach -outform DER -out test/signed_message.txt -nocerts -engine rtengine

2.1) проверка подписи с помощью OpenSSL

./openssl cms -engine rtengine -verify -certfile test/cert.pem -CAfile test/cert.pem -in test/signed_message.txt -inform der
# OK 

3) Далее пример с импортом сертификата на токен (тогда его не надо указывать в VerifyParams) должен сработать. Cертификат на токене используется как сертификат доверенного УЦ, поэтому пользовательский сертификат все равно придется добавлять в VerifyParams. Для самоподписанного сертификата это одно и тоже.

/*************************************************************************
* Rutoken                                                                *
* Copyright (c) 2003-2023, Aktiv-Soft JSC. All rights reserved.          *
* Подробная информация:  http://www.rutoken.ru                           *
*------------------------------------------------------------------------*
* Данный пример демонстрирует проверку подписи CMS сообщения с помощью   *
* pkicore. CMS сообщение с данными и сертификатом читается из файла      *
* signed_data. CA сертификат читается из файла ca.pem. Для создания CMS  *
* сообщения можно воспользоваться примером CmsSign.                      *
*************************************************************************/

#include <common.h>

using namespace std;
using namespace rutoken::pkicore;

int main()
{
    try {
        cout << boolalpha;

        /**********************************************************************
        * Инициализируем pkicore, передав путь до директории с библиотекой    *
        * rtPKCS11ECP.                                                        *
        **********************************************************************/
        rutoken::pkicore::initialize(".");
        SCOPE_EXIT() {
            /**********************************************************************
            * Завершаем работу с pkicore при выходе из текущего блока.            *
            **********************************************************************/
            rutoken::pkicore::deinitialize();
        };

        /**********************************************************************
        * Получаем список подключенных устройств и продолжаем работу с первым *
        * доступным устройством.                                              *
        **********************************************************************/
        auto devices = Pkcs11Device::enumerate();
        if (devices.empty()) {
            throw runtime_error("There must be at least one device connected");
        }

        auto device = move(devices.front());

        /**********************************************************************
        * Читаем файл с подписанным сообщением.                               *
        **********************************************************************/
        const auto inputFileName = "test/signed_message.txt";

        cout << "Reading signed message from " << inputFileName << endl;

        auto signedData = cms::SignedData::parse(readFile(inputFileName));

        /**********************************************************************
        * Читаем файл с CA сертификатом.                                      *
        **********************************************************************/
        const auto caCertFileName = "test/cert.der";

        cout << "Reading CA certificate from " << caCertFileName << endl;

        auto caCertBuf = readFile(caCertFileName);
        ExternalCert caCert(caCertBuf.data(), caCertBuf.size());

        device.login("12345678");
        SCOPE_EXIT(&device) {
            /**********************************************************************
            * Сбрасываем права доступа при выходе из текущего блока.              *
            **********************************************************************/
            device.logout();
        };
        device.importCert(caCert, true);

        ExternalCert userCert(caCertBuf.data(), caCertBuf.size());

        /**********************************************************************
        * Проверяем подпись сообщения.                                        *
        **********************************************************************/
        cms::VerifyParams params(device, cms::VerifyParams::Flag::verifyUserCert);
        params.addUserCert(userCert);

        auto result = signedData.verify(params);
        if (result == cms::VerifyResult::success) {
            cout << "Correct signature" << endl;
        } else {
            cout << "Wrong signature" << endl;
        }
    } catch (const exception& e) {
        cerr << e.what() << endl;
        return 1;
    }

    return 0;
}

4) Пример без импорта сертификата на токен

/*************************************************************************
* Rutoken                                                                *
* Copyright (c) 2003-2023, Aktiv-Soft JSC. All rights reserved.          *
* Подробная информация:  http://www.rutoken.ru                           *
*------------------------------------------------------------------------*
* Данный пример демонстрирует проверку подписи CMS сообщения с помощью   *
* pkicore. CMS сообщение с данными и сертификатом читается из файла      *
* signed_data. CA сертификат читается из файла ca.pem. Для создания CMS  *
* сообщения можно воспользоваться примером CmsSign.                      *
*************************************************************************/

#include <common.h>

using namespace std;
using namespace rutoken::pkicore;

int main()
{
    try {
        cout << boolalpha;

        /**********************************************************************
        * Инициализируем pkicore, передав путь до директории с библиотекой    *
        * rtPKCS11ECP.                                                        *
        **********************************************************************/
        rutoken::pkicore::initialize(".");
        SCOPE_EXIT() {
            /**********************************************************************
            * Завершаем работу с pkicore при выходе из текущего блока.            *
            **********************************************************************/
            rutoken::pkicore::deinitialize();
        };

        /**********************************************************************
        * Получаем список подключенных устройств и продолжаем работу с первым *
        * доступным устройством.                                              *
        **********************************************************************/
        auto devices = Pkcs11Device::enumerate();
        if (devices.empty()) {
            throw runtime_error("There must be at least one device connected");
        }

        auto device = move(devices.front());

        /**********************************************************************
        * Читаем файл с подписанным сообщением.                               *
        **********************************************************************/
        const auto inputFileName = "test/signed_message.txt";

        cout << "Reading signed message from " << inputFileName << endl;

        auto signedData = cms::SignedData::parse(readFile(inputFileName));

        /**********************************************************************
        * Читаем файл с CA сертификатом.                                      *
        **********************************************************************/
        const auto caCertFileName = "test/cert.der";

        cout << "Reading CA certificate from " << caCertFileName << endl;

        auto caCertBuf = readFile(caCertFileName);
        ExternalCert caCert(caCertBuf.data(), caCertBuf.size());
        ExternalCert userCert(caCertBuf.data(), caCertBuf.size());

        /**********************************************************************
        * Проверяем подпись сообщения.                                        *
        **********************************************************************/
        cms::VerifyParams params(device, cms::VerifyParams::Flag::verifyUserCert);
        params.addCaCert(caCert);
        params.addUserCert(userCert);

        auto result = signedData.verify(params);
        if (result == cms::VerifyResult::success) {
            cout << "Correct signature" << endl;
        } else {
            cout << "Wrong signature" << endl;
        }
    } catch (const exception& e) {
        cerr << e.what() << endl;
        return 1;
    }

    return 0;
}

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

Спасибо! Это помогло, с небольшим уточнением что в команде проверки подписи OpenSSL нужно добавить флаг -noverify, иначе ругается на самоподписанный сертификат, получается так:

openssl cms -engine rtengine -verify -certfile test/cert.pem -CAfile test/cert.pem -in test/signed_message.txt -inform der -noverify

Еще остается вопрос как вытащить непосредственно подписанные данные из экземпляра rutoken::pkicore::cms::SignedData (в виде строки или байтового массива, значения не имеет). Или придется руками парсить файл?

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

nicetas.matthias пишет:

нужно добавить флаг -noverify, иначе ругается на самоподписанный сертификат

Добрый день!

Это странно. Указан **-CAfile test/cert.pem**, то есть сертификат указан как доверенный, поэтому **-noverify** требоваться не должен: сертификат подписанта по цепочке должен успешно проверяться.

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

Добрый день!
Извиняюсь, мой недосмотр. Добавил -CAfile и все заработало без -noverify.
Но вопрос по поводу того как вытащить данные в программу остается

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

Добрый день!

Как вытащить непосредственно подписанные данные из экземпляра rutoken::pkicore::cms::SignedData (в виде строки или байтового массива, значения не имеет).

Такого API в pki-core на данный момент нет. Можем предложить вам использовать detached CMS, когда исходный документ и файл с ЭП - это разные файлы. Еще вариант - использовать API OpenSSL для парсинга attached CMS.

В целом будем рады узнать в hotline@rutoken.ru больше о вашем проекте/продукте.

Re: Рутокен ЭЦП 3.0 с pki-core (C++) и OpenSSL

Спасибо за оперативную помощь!