Введение.
В одной из поддерживаемых мной систем цифровая подпись сообщений проверялась с помощью КриптоПро 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, то большая часть примеров из официальной документации подходит идля “КриптоПро”. Чем я собствеено говоря и пользовался при написании модуля, так как с примерами у самого КриптоПро не очень все хорошо.
Загрузка сертификата в хранилище.
Для того, чтобы загрузить сертификат в хранилище нужно выполнить следующие шаги:
- Считать сертификат из файла
- Открыть хранилище сертификатов
- Положить в него сертификат
- Закрыть хранилище
Тут меня ждал первый ньюанс, что в 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
.
Полезные ссылки:
- Документация по КриптоПро
- Cryptography Reference MS
- CryptoAPI: How to import a certificate
- ЭЦП в браузере
- py_cpcsp