Проверка подписи КриптоПро на Python

Введение.

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

Теоритическая часть.

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

Все ключи крипто про хранит в своих хранилищах, таких как root, ca, my. Чтобы в них загрузить сертификат в поставке Криптопро CSP идет специальная утилита certmgr. Синтаксих ее работы таков:

certmgr -inst -store <имя хранилища> -file <файл с сертификатом>

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

certmgr -instl -crl -store <имя хранилища> -file <CRL файл>

Побробную информацию по работе этой утилиты можно получить вызвав:

certmgr -help

Для конкретного сертификата также можно проверить цепочку. Делается это командой:

cryptcp -verify -f <файл сертификата> "text" -errchain

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

Из всего вышесказанного я подумал, что в библиотеке мне нужны будут следующие функции:

  • Загрузка сертификата в хранилище;
  • Загрузка файла отозванных сертификатов;
  • Постороение цепочки сертификата;
  • Проверка подписи.

Реализация взаимодействия с КриптоПро CSP на C.

Так как КриптоПро CSP(CPCSP) является доработкой CryptoApi от Microsoft, то большая часть примеров из официальной документации подходит идля “КриптоПро”. Чем я собствеено говоря и пользовался при написании модуля, так как с примерами у самого КриптоПро не очень все хорошо.

Загрузка сертификата в хранилище.

Для того, чтобы загрузить сертификат в хранилище нужно выполнить следующие шаги:

  1. Считать сертификат из файла
  2. Открыть хранилище сертификатов
  3. Положить в него сертификат
  4. Закрыть хранилище

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

typedef struct CERT {
    BYTE *content;
    DWORD size;
} CERT;

CERT readFile(char *filename)
{
    CERT cert = {NULL, 0};
    FILE *fCert;

    fCert = fopen(filename, "r");
    if (fCert)
    {
		fseek(fCert, 0, SEEK_END);   
		cert.size = ftell(fCert);
		rewind(fCert);

	    cert.content = (unsigned char *)malloc(cert.size * sizeof(unsigned char));
	    fread(cert.content, cert.size, 1, fCert);
    }
    else
    {
		perror("Error open certificate file");
    }
    fclose(fCert);

  return cert;
}

PCCERT_CONTEXT ReadCertificateFromFile(char *filename)
{
    CERT fileCert; 
    PCCERT_CONTEXT cert = NULL;

    fileCert = readFile(filename);
		
    cert = CertCreateCertificateContext(
		X509_ASN_ENCODING,
		fileCert.content,
		fileCert.size
	);
    
    if (!(cert))
    {
		perror("Error create cert");
    }

  return cert;
}

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

Далее в описании функций будут использоваться следующие коды ошибок:

# define OPERATION_SUCCESS        0
# define OPEN_STORE_ERROR         1
# define ADD_CERT_TO_STORE_ERROR  2
# define CLOSE_STORE_ERROR        3
# define ADD_CRL_TO_STORE_ERROR   4
# define STR_TO_BIN_LEN_ERROR     5
# define STR_TO_BIN_CONTENT_ERROR 6
# define VERIFY_MSG_SIGNATURE     7
# define GET_CERT_CHAIN_ERROR     8
# define READ_CERT_ERROR          9
# define READ_CRL_ERROR           10

Функция загрузки сертификата в хранилище будет выглядеть следующим образом:

int LoadCertificateToSystemStore(char *cert_file_path, char *store_name)
{
    HCERTSTORE     cpcsp_cert_store = NULL;
    PCCERT_CONTEXT cert_context;

    cert_context = ReadCertificateFromFile(cert_file_path);
    if (!cert_context)
	return READ_CERT_ERROR;
  
    cpcsp_cert_store = CertOpenSystemStore(0, store_name);

    if (!cpcsp_cert_store)
	return OPEN_STORE_ERROR;

    if (!CertAddCertificateContextToStore(
	    cpcsp_cert_store,
	    cert_context,
	    CERT_STORE_ADD_REPLACE_EXISTING,
	    NULL))
	return ADD_CERT_TO_STORE_ERROR;

    if (!CertCloseStore(cpcsp_cert_store, 0))
	return CLOSE_STORE_ERROR;

    if (cert_context)
	CertFreeCertificateContext(cert_context);

    return OPERATION_SUCCESS;
}

В этой функции считывается файл сертификата (функция ReadCertificateFromFile), затем открываем системное хранилище методом CertOpenSystemStore. Если системное хранилище открылось успешно, то с помощью метода CertAddCertificateContextToStore, сертификат загрузается в хранилище. И в заключении хранилище закрывается функцией CertCloseStore.

Нужно отметить что функция CertOpenSystemStore ипользуется только для чтения системных хранилищ (root, ca, my), для остальных надо использовать CertOpenStore.

Загрузка файла отозванных сертификатов.

Функции чтения списка отозванных сертификатов(CRL) и загрузки их в хранилище идентичны функциям работы с сертификатами, за тем исключением, что для их чтения и загрузки используются функции CPCSP c CRL вместо Certificate в названии функции. Например CertAddCertificateContextToStore будет выглядеть как CertAddCRLContextToStore.

Таким образом код для загруки CRL будет таким:

int LoadCRLToSystemStore(char *cert_file_path, char *store_name)
{
    HCERTSTORE    cpcsp_cert_store = NULL;
    PCCRL_CONTEXT crl_context;

    crl_context = ReadCRLFromFile(cert_file_path);
    if (!crl_context)
	return READ_CRL_ERROR;
  
    cpcsp_cert_store = CertOpenSystemStore(0, store_name);

    if (!cpcsp_cert_store)
	return OPEN_STORE_ERROR;

    if (!CertAddCRLContextToStore(
	    cpcsp_cert_store,
	    crl_context,
	    CERT_STORE_ADD_REPLACE_EXISTING,
	    NULL))
	return ADD_CRL_TO_STORE_ERROR;

    if (!CertCloseStore(cpcsp_cert_store, 0))
	return CLOSE_STORE_ERROR;

    if (crl_context)
	CertFreeCRLContext(crl_context);

  return OPERATION_SUCCESS;
}

Постороение цепочки сертификата.

Код функции проверки цепочки сертификатов выглядит следующим образом

int VerifyCertChain(char *certFilePath)
{
    PCCERT_CONTEXT           pCertContext;
    PCCERT_CHAIN_CONTEXT     pChainContext;
    CERT_ENHKEY_USAGE        EnhkeyUsage;
    CERT_USAGE_MATCH         CertUsage;
    CERT_CHAIN_PARA          ChainPara;

    /*
      инициализация парметров поиска и сопоставления, которые 
      будут использоваться для построения цепочки сертификатов 
    */
    EnhkeyUsage.cUsageIdentifier = 0;
    EnhkeyUsage.rgpszUsageIdentifier = NULL;
    CertUsage.dwType = USAGE_MATCH_TYPE_AND;
    CertUsage.Usage  = EnhkeyUsage;
    ChainPara.cbSize = sizeof(CERT_CHAIN_PARA);
    ChainPara.RequestedUsage=CertUsage;
 
    pCertContext = ReadCertificateFromFile(certFilePath);

    if (!CertGetCertificateChain(
	    NULL,
	    pCertContext,
	    NULL,
	    NULL,
	    &ChainPara,
	    0,
	    NULL,
	    &pChainContext))
    {
		perror("The chain could not be created");
    }
	
	int result = pChainContext->TrustStatus.dwErrorStatus;

    if (pChainContext)
    {
		CertFreeCertificateChain(pChainContext);
    }
	
    return result;
}

Помимо настроек цепочки, тут вызывается функция CertGetCertificateChain, которая формирует собственно цепочку сертификатов и записывает ее в структуру PCCERT_CHAIN_CONTEXT. В данной структуре поле TrustStatus отвечает за статус опреации, если цепочка построена корректно, то dwErrorStatus будет 0, иначе будет записан код ошибки.

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

Для начала я подумал сорфировать самоподписной сертификат для проверки функционирования функции, но оказалось, что CPCSP не поддерживает их, поэтому я создал сертификат в Тестовом УЦ КриптоПро. Я не буду описывать данный процесс, так как к библиотике он имеет посредственное отношение. Только скажу, что файл подписи я генерировал под Windows, потому как это было проще сделать через КриптоПро ЭЦП Browser plug-in.

Также надо отметить, что сертификат ЦС, надо загрузить в хранилище “Доверенные корневые…". Иначе сгенерированный тестовый сертификат не установиться и плагин для ЭЦП не будет корректно работать. Код функции проверки подписи приведен ниже:

int VerifySignedMessage(char *signature)
{
    DWORD blob_size = 0;
    /* 
       определяем размер выходного der блоба
       для подписанного сообщения
    */
    if (!CryptStringToBinaryA(
	    signature,
	    strlen(signature),
	    CRYPT_STRING_BASE64,
	    NULL,
	    &blob_size,
	    NULL,
	    NULL))
	return STR_TO_BIN_LEN_ERROR;	


    /*
       заполняем блоб подписанного сообщения
     */
    BYTE *msg_blob;
    msg_blob = (BYTE *)malloc(blob_size);
    if (!CryptStringToBinaryA(
	    signature,
	    strlen(signature),
	    CRYPT_STRING_BASE64,
	    msg_blob,
	    &blob_size,
	    NULL,
	    NULL))
	return STR_TO_BIN_CONTENT_ERROR;	

    /*
      выполняем проверку подписи
     */
    CRYPT_VERIFY_MESSAGE_PARA verify_params;

    verify_params.cbSize = sizeof(CRYPT_VERIFY_MESSAGE_PARA);
    verify_params.dwMsgAndCertEncodingType = ENCODING_TYPE;
    verify_params.hCryptProv = 0;
    verify_params.pfnGetSignerCertificate = NULL;
    verify_params.pvGetArg = NULL;
    
    if(!CryptVerifyMessageSignature(
        &verify_params,
        0,
        msg_blob,
        blob_size,
        NULL,
        NULL,
        NULL))
	return VERIFY_MSG_SIGNATURE;
    
    return OPERATION_SUCCESS;
}

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

Также надо отметить что функция CryptStringToBinaryA вызывается 2 раза, первый для получения размер подписи, а второй, чтобы получить данные раскодированные из base64 данные. Ну и затем подпись соответственно проверяется.

Создание python библиотеки.

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

#ifdef __linux__
	#include <Python.h>
#elif __APPLE__
	#include <Python/Python.h>
#endif

#ifndef LIBSIGNATURE_H_
#define LIBSIGNATURE_H_

/* 
   Список экспортируемых функций
 */
PyObject * PyLoadCertificate(PyObject *self, PyObject *args);
PyObject * PyLoadCRL(PyObject *self, PyObject *args);
PyObject * PyVerifyCertChain(PyObject *self, PyObject *args);
PyObject * PyVerifySignedMessage(PyObject *self, PyObject *args);

/*
  Типы исключений различных ситуаций
 */
extern PyObject *PyOpenStoreError;
extern PyObject *PyAddCertToStoreError;
extern PyObject *PyCloseStoreError;
extern PyObject *PyAddCrlToStoreError;
extern PyObject *PyStrToBinLenError;
extern PyObject *PyStrToBinContentError;
extern PyObject *PyVerifyMsgSignatureError;
extern PyObject *PyGetCertChainError;
extern PyObject *PyReadCertError;
extern PyObject *PyReadCrlError;

#endif

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

Код оберки для библиотеки выглядит следующим образом:

#include <stdio.h>
#include "libsignature.h"

// Таблица методов реализуемых расширением
// название, функция, параметры, описание
static PyMethodDef LibsignatueMethods[] = {
    {"load_certificate",  PyLoadCertificate, METH_VARARGS, NULL},
    {"load_crl",  PyLoadCRL, METH_VARARGS, NULL},
    {"verify_chain_certificate",  PyVerifyCertChain, METH_VARARGS, NULL},
    {"vefigy_signature",  PyVerifySignedMessage, METH_VARARGS, NULL},
    {NULL, NULL,  0, NULL}
};

PyObject *PyOpenStoreError;
PyObject *PyAddCertToStoreError;
PyObject *PyCloseStoreError;
PyObject *PyAddCrlToStoreError;
PyObject *PyStrToBinLenError;
PyObject *PyStrToBinContentError;
PyObject *PyVerifyMsgSignatureError;
PyObject *PyGetCertChainError;
PyObject *PyReadCertError;
PyObject *PyReadCrlError;

// Инициализация
PyMODINIT_FUNC initlibsignature(void)
{
    PyObject *m;

    // Инизиализруем модуль libsignature
    m = Py_InitModule("libsignature", LibsignatueMethods);
    if (m == NULL)
        return;

    // Создание исключений при работе с расширением
    PyOpenStoreError = PyErr_NewException("libsignature.OpenStoreError",
	NULL,
	NULL
	);
    PyAddCertToStoreError = PyErr_NewException(
	"libsignature.AddCertToStoreError",
	NULL,
	NULL
	);
    PyCloseStoreError = PyErr_NewException(
	"libsignature.CloseStoreError",
	NULL,
	NULL
	);
    PyAddCrlToStoreError = PyErr_NewException(
	"libsignature.AddCRLToStoreError",
	NULL,
	NULL
	);
    PyStrToBinLenError = PyErr_NewException(
	"libsignature.StrToBinLenError",
	NULL,
	NULL
	);
    PyStrToBinContentError = PyErr_NewException(
	"libsignature.StrToBinContentError",
	NULL,
	NULL
	);
    PyVerifyMsgSignatureError = PyErr_NewException(
	"libsignature.VerifySignError",
	NULL,
	NULL
	);
    PyGetCertChainError = PyErr_NewException(
	"libsignature.ChainCertError",
	NULL,
	NULL
	);
    PyReadCertError = PyErr_NewException(
	"libsignature.ReadCertError",
	NULL,
	NULL
	);
    PyReadCrlError = PyErr_NewException(
	"libsignature.ReadCRLError",
	NULL,
	NULL
	);

    Py_INCREF(PyOpenStoreError);
    Py_INCREF(PyAddCertToStoreError);
    Py_INCREF(PyCloseStoreError);
    Py_INCREF(PyAddCrlToStoreError);
    Py_INCREF(PyStrToBinLenError);
    Py_INCREF(PyStrToBinContentError);
    Py_INCREF(PyVerifyMsgSignatureError);
    Py_INCREF(PyGetCertChainError);
    Py_INCREF(PyReadCertError);
    Py_INCREF(PyReadCrlError);

    PyModule_AddObject(m, "error", PyOpenStoreError);
    PyModule_AddObject(m, "error", PyAddCertToStoreError);
    PyModule_AddObject(m, "error", PyCloseStoreError);
    PyModule_AddObject(m, "error", PyAddCrlToStoreError);
    PyModule_AddObject(m, "error", PyStrToBinLenError);
    PyModule_AddObject(m, "error", PyStrToBinContentError);
    PyModule_AddObject(m, "error", PyVerifyMsgSignatureError);
    PyModule_AddObject(m, "error", PyGetCertChainError);
    PyModule_AddObject(m, "error", PyReadCertError);
    PyModule_AddObject(m, "error", PyReadCrlError);
}

Что делается в этом файле подробно описано здесь.

Для проверки работоспособности питоновской библиотеки, напишем следующий тест:

import libsignature


class TestLibSignature(unittest.TestCase):
    """
    Класс для тестирования работы с КриптоПро CSP.
    """
    def setUp(self):
        """
        Задание путей до тестовых файлов
        """
        self._cert_file = os.path.join(current_path, "files/ca.cer")
        self._crl_file = os.path.join(current_path, "files/ca.crl")
        self._user_cert = os.path.join(current_path, "files/user.cer")
        self._sig_file = os.path.join(current_path, "files/test_sign.sig") 
        self._store = "ROOT"

    def test_load_certificate(self):
        """
        Проверка загрузки сертификата.
        """
        result = libsignature.load_certificate(self._cert_file, self._store)
        self.assertIsNone(result)

    def test_load_crl(self):
        """
        Проверка загрузки списка отозванных серитификатов.
        """
        result = libsignature.load_crl(self._crl_file, self._store)
        self.assertIsNone(result)

    def test_verify_cert_chain(self):
        """
        Проверка корректности цепочки сертификатов
        """
        result = libsignature.verify_chain_certificate(self._user_cert)
        self.assertIsNone(result)

    def verify_signature(self):
        """
        Проверка подписи сообщения
        """
        with open(self._sig_file, "rb") as sigfile:
            signature = sigfile.read()
            result = libsignature.vefigy_signature(signature)
            self.assertIsNone(result)


if __name__ == '__main__':
    unittest.main()

Теперь все готово, и можно запусть команду make test для проверки работоспособности.

Заключение.

Процесс создания библиотеки получился трудоемкий, но на выходе получилась рабочая библиотека, которой можно пользоваться. В репозиотории можете найти пакет для работы из python, но также можно использовать только C-ную часть. Для работы С библиотеки нужно выполнить make build_c.

Полезные ссылки:

  1. Документация по КриптоПро
  2. Cryptography Reference MS
  3. CryptoAPI: How to import a certificate
  4. ЭЦП в браузере
  5. py_cpcsp
 
comments powered by Disqus