SUPPORT-8592. Проверка подписи маркера доступа

This commit is contained in:
Наиля Алашкова 2024-11-14 12:16:55 +03:00
parent 9e85e4e8de
commit 4fa45a1f5e
46 changed files with 1970 additions and 250 deletions

View file

@ -1,9 +1,9 @@
CMAKE_MINIMUM_REQUIRED (VERSION 3.0)
PROJECT (ervu-sign-module VERSION 1.0.0 LANGUAGES C)
SET (CMAKE_C_COMPILER "gcc")
PROJECT (ervu-sign-module VERSION 1.1.0 LANGUAGES C)
IF (CMAKE_VERBOSE)
SET (CMAKE_VERBOSE_MAKEFILE 1)
ENDIF ()
@ -49,8 +49,30 @@ SET (DEP_LIBS
-lfcgi
-lglib-2.0
-ldl
-ljson-glib-1.0
-lgobject-2.0
)
# JSON-GLIB
MESSAGE ("")
MESSAGE ("Try to find the JSON-glib-1.0..")
pkg_check_modules (JSONGLIB REQUIRED json-glib-1.0)
IF (NOT JSONGLIB_FOUND)
MESSAGE(SEND_ERROR "Can not find json-glib-1.0")
ELSE ()
MESSAGE ("json-glib-1.0 is found: ")
MESSAGE ("JSONGLIB_LIB_INCLUDE_DIR : " ${JSONGLIB_LIB_INCLUDE_DIR})
MESSAGE ("JSONGLIB_INCLUDE_DIR : " ${JSONGLIB_INCLUDE_DIR})
MESSAGE ("JSONGLIB_LIBRARY_DIR : " ${JSONGLIB_LIBRARY_DIR})
MESSAGE ("JSONGLIB_INCLUDE_DIRS : " ${JSONGLIB_INCLUDE_DIRS})
MESSAGE ("JSONGLIB_LIBRARY_DIRS : " ${JSONGLIB_LIBRARY_DIRS})
MESSAGE ("JSONGLIB_LIBRARIES : " ${JSONGLIB_LIBRARIES})
ENDIF (NOT JSONGLIB_FOUND)
MESSAGE ("")
INCLUDE_DIRECTORIES ("${JSONGLIB_INCLUDE_DIRS}")
# version.h
CONFIGURE_FILE (${SOURCE_DIR}/version.h.in ${SOURCE_DIR}/version.h)
@ -76,6 +98,14 @@ INCLUDE_DIRECTORIES ("${CRYPTOPRO_INCLUDE_DIRS}")
FILE (GLOB_RECURSE HEADERS "${SOURCE_DIR}/*.h")
# tests
option(WITH_TESTS "Build with tests" OFF)
IF (WITH_TESTS)
MESSAGE ("Build with tests: ON")
enable_testing()
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/tests/)
ENDIF ()
ADD_EXECUTABLE (${PROJECT_NAME}
${HEADERS}
${SOURCE_DIR}/main.c
@ -88,6 +118,9 @@ ADD_EXECUTABLE (${PROJECT_NAME}
${UTILS_DIR}/conf_file_context.c
${UTILS_DIR}/cryptopro.c
${UTILS_DIR}/gconf_file.c
${UTILS_DIR}/glib_utils.c
${UTILS_DIR}/json_parser.c
${UTILS_DIR}/jwt.c
${UTILS_DIR}/library.c
${UTILS_DIR}/logger.c
${UTILS_DIR}/str_t.c
@ -97,6 +130,8 @@ ADD_EXECUTABLE (${PROJECT_NAME}
${FCGISRV_DIR}/fcgi_utils.c
${FCGISRV_DIR}/fcgi_worker.c
${MODULES_DIR}/service_sign.c
${MODULES_DIR}/service_verify.c
${MODULES_DIR}/service_version.c
)
TARGET_LINK_LIBRARIES (${PROJECT_NAME} ${DEP_LIBS})

View file

@ -13,7 +13,8 @@ RUN apt-get update \
cmake \
make \
gcc \
gcc10
gcc10 \
libjson-glib libjson-glib-devel
WORKDIR /build
COPY src src
@ -32,7 +33,7 @@ ENV TZ=Europe/Moscow
COPY entrypoint.sh /entrypoint.sh
RUN apt-get update \
&& apt-get -y install glib2 libfcgi \
&& apt-get -y install glib2 libfcgi libjson-glib \
&& apt-get clean \
&& rm -f /var/cache/apt/*.bin \
&& rm -f /var/lib/apt/lists/update* \
@ -50,6 +51,18 @@ COPY --from=builder /build/.build/ervu-sign-module /opt/ervu-sign-module/ervu-si
EXPOSE 9009
ARG ESIA_CA_CERT=test_ca_rtk3.cer
COPY conf/${ESIA_CA_CERT} ${ESIA_CA_CERT}
RUN /opt/cprocsp/bin/amd64/certmgr -install -store mRoot -file "${ESIA_CA_CERT}"
USER ervu
ARG ESIA_CERT="TESIA GOST 2012 new.cer"
ARG ESIA_CA_CRL=b0fd8eb959d9489d5b7b4c143a06cad7952a0744.crl
COPY --chown=ervu:ervu conf/${ESIA_CERT} ${ESIA_CERT}
COPY --chown=ervu:ervu conf/${ESIA_CA_CRL} ${ESIA_CA_CRL}
RUN /opt/cprocsp/bin/amd64/certmgr -install -file "${ESIA_CERT}" \
&& /opt/cprocsp/bin/amd64/certmgr -install -store uCA -crl -file "${ESIA_CA_CRL}"
ENTRYPOINT ["/entrypoint.sh"]

View file

@ -1,6 +1,10 @@
## Краткое описание
Модуль предназначен для подписи данных.
В модуле реализованы следующие функции:
- подпись данных
- проверка подписи маркера доступа
### Подпись данных
Приложение принимает POST-запрос по протоколу FastCGI (Content-Type: text/plain).
Подписывает строку, полученную в теле запроса.
@ -28,6 +32,35 @@ $ curl -v http://127.0.0.1:8080/sign -H "Content-Type: text/plain" -d "test"
urlSafeBase64_of_signed_string_test
```
### Проверка подписи маркера доступа
Приложение принимает POST-запрос по протоколу FastCGI (Content-Type: text/plain).
Проверяет подпись маркера доступа, полученного в теле запроса.
В ответе возвращает один из следующих статус-кодов:
- 200 OK - подпись валидна
- 401 Unauthorized - подпись невалидна (в теле ответа возвращается код ошибки от криптопровайдера)
- 500 Internal Server Error - внутренняя ошибка сервера
Пример выполнения запроса:
```
$ curl -v http://127.0.0.1:8080/verify -H "Content-Type: text/plain" -d "some_valid_access_token"
* Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> POST /sign HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.4.0
> Accept: */*
> Content-Type: text/plain
> Content-Length: 4
>
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Wed, 16 Oct 2024 09:55:46 GMT
< Transfer-Encoding: chunked
< Connection: keep-alive
<
```
## Сборка из исходников
Инструкции по сборке из исходников см. "Инструкция по сборке.md"
@ -46,8 +79,9 @@ cmake -DCONFIG_NAME=/opt/ervu-sign-module.conf ..
Приложение настраивается в конфигурационном файле, заданном на этапе сборки (по умолчанию, /etc/ervu-sign-module.conf).
- В секции **\[main\]** задать количество воркеров
worker_processes = 10 *\# значение по умолчанию: 10*
- В секции **\[main\]** задать общие настройки:
worker_processes = 10 *\# количество воркеров (значение по умолчанию: 10)*
cp_file = libcapi20.so *\# путь до файла библиотеки криптопровайдера*
- В секции **\[fcgi\]** задать настройки fcgi-сервера:
fcgi_listen_port = 9009 *\# значение по умолчанию: 9009, должно совпадать со значением в nginx.conf*
@ -55,8 +89,10 @@ fcgi_listen_host = 127.0.0.1 *\# значение по умолчанию: 127.0
fcgi_thread_pool_size = 1 *\# значение по умолчанию: 1*
- В секции **\[sign\]** задать настройки модуля подписания:
location = /sign *\# значение по умолчанию: /sign, должно совпадать со значением в nginx.conf*
cp_file = libcapi20.so *\# путь до файла библиотеки криптопровайдера*
signer_subject = signer@example.ru *\# email, ИНН, СНИЛС или любая другая строка из свойства контейнера «Субъект»*
pin = \*\*\*\* *\# пароль от контейнера*
location = /sign *\# значение по умолчанию: /sign*
sign_cert_thumbprint = sha1_thumbprint_of_signer_cert *\# SHA1 отпечаток сертификата, которым ИС подписывает секрет*
sign_cert_password = \*\*\*\* *\# пароль от контейнера*
- В секции **\[verify\]** задать настройки проверки подписи маркера доступа:
location = /verify *\# значение по умолчанию: /verify*
esia_cert_thumbprint = sha1_thumbprint_of_esia_cert *\# SHA1 отпечаток сертификата ЕСИА*

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,6 @@
[main]
#worker_processes = 10
cp_file = /opt/cprocsp/lib/amd64/libcapi20.so
[fcgi]
fcgi_listen_port = 9009
@ -8,6 +9,9 @@ fcgi_listen_host = 127.0.0.1
[sign]
#location = /sign
cp_file = /opt/cprocsp/lib/amd64/libcapi20.so
signer_subject = signer@example.ru
pin = ****
sign_cert_thumbprint =
sign_cert_password = ****
[verify]
#location = /verify
esia_cert_thumbprint =

View file

@ -2,8 +2,8 @@ server {
listen 80;
server_name localhost;
location = /sign {
fastcgi_pass ervu-sign-module:9009;
include fastcgi_params;
location / {
fastcgi_pass ervu-sign-module:9009;
include fastcgi_params;
}
}

View file

@ -35,7 +35,7 @@ http {
listen 80;
server_name localhost;
location = /sign {
location / {
fastcgi_pass localhost:9009;
include fastcgi_params;
}

36
conf/test_ca_rtk3.cer Normal file
View file

@ -0,0 +1,36 @@
-----BEGIN CERTIFICATE-----
MIIGLTCCBdqgAwIBAgIRAtGQqQCIsFiWS3yUT9DfFhcwCgYIKoUDBwEBAwIwggGB
MRUwEwYFKoUDZAQSCjc3MDcwNDkzODgxGDAWBgUqhQNkARINMTAyNzcwMDE5ODc2
NzELMAkGA1UEBhMCUlUxKTAnBgNVBAgMIDc4INCh0LDQvdC60YIt0J/QtdGC0LXR
gNCx0YPRgNCzMSYwJAYDVQQHDB3QodCw0L3QutGCLdCf0LXRgtC10YDQsdGD0YDQ
szGBnjCBmwYDVQQJDIGT0LzRg9C90LjRhtC40L/QsNC70YzQvdGL0Lkg0L7QutGA
0YPQsyDQodC80L7Qu9GM0L3QuNC90YHQutC+0LUg0JLQnS7QotCV0KAu0JMuLCDQ
odC40L3QvtC/0YHQutCw0Y8g0L3QsNCx0LXRgNC10LbQvdCw0Y8sINC00L7QvCAx
NCwg0LvQuNGC0LXRgNCwINCQMSYwJAYDVQQKDB3Qn9CQ0J4gItCg0L7RgdGC0LXQ
u9C10LrQvtC8IjElMCMGA1UEAwwc0KLQtdGB0YLQvtCy0YvQuSDQo9CmINCg0KLQ
mjAeFw0yMzA5MjUxMDA3MjJaFw0zODA5MjUxMDA3MjJaMIIBgTEVMBMGBSqFA2QE
Ego3NzA3MDQ5Mzg4MRgwFgYFKoUDZAESDTEwMjc3MDAxOTg3NjcxCzAJBgNVBAYT
AlJVMSkwJwYDVQQIDCA3OCDQodCw0L3QutGCLdCf0LXRgtC10YDQsdGD0YDQszEm
MCQGA1UEBwwd0KHQsNC90LrRgi3Qn9C10YLQtdGA0LHRg9GA0LMxgZ4wgZsGA1UE
CQyBk9C80YPQvdC40YbQuNC/0LDQu9GM0L3Ri9C5INC+0LrRgNGD0LMg0KHQvNC+
0LvRjNC90LjQvdGB0LrQvtC1INCS0J0u0KLQldCgLtCTLiwg0KHQuNC90L7Qv9GB
0LrQsNGPINC90LDQsdC10YDQtdC20L3QsNGPLCDQtNC+0LwgMTQsINC70LjRgtC1
0YDQsCDQkDEmMCQGA1UECgwd0J/QkNCeICLQoNC+0YHRgtC10LvQtdC60L7QvCIx
JTAjBgNVBAMMHNCi0LXRgdGC0L7QstGL0Lkg0KPQpiDQoNCi0JowZjAfBggqhQMH
AQEBATATBgcqhQMCAiMBBggqhQMHAQECAgNDAARASD1IrLJ64NR5ZKV3lu00YpyS
A7W9tOU/DUjlpjKHcqAsDml/B/XX26zG7KocnXLuTaJLR3c7j2FJkRVTTI20JaOC
AiAwggIcMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUsP2OuVnZSJ1be0wUOgbK
15UqB0QwEgYDVR0TAQH/BAgwBgEB/wIBADAlBgNVHSAEHjAcMAYGBFUdIAAwCAYG
KoUDZHEBMAgGBiqFA2RxAjCCAToGBSqFA2RwBIIBLzCCASsMMiLQmtGA0LjQv9GC
0L7Qn9GA0L4gQ1NQIDQuMCBSNCIgKNCy0LXRgNGB0LjRjyA0LjApDFMi0KPQtNC+
0YHRgtC+0LLQtdGA0Y/RjtGJ0LjQuSDRhtC10L3RgtGAICLQmtGA0LjQv9GC0L7Q
n9GA0L4g0KPQpiIg0LLQtdGA0YHQuNC4IDIuMAxP0KHQtdGA0YLQuNGE0LjQutCw
0YIg0YHQvtC+0YLQstC10YLRgdGC0LLQuNGPIOKEliDQodCkLzEyNC0zOTcxINC+
0YIgMTUuMDEuMjAyMQxP0KHQtdGA0YLQuNGE0LjQutCw0YIg0YHQvtC+0YLQstC1
0YLRgdGC0LLQuNGPIOKEliDQodCkLzEyOC00Mzc2INC+0YIgMjguMTAuMjAyMjA9
BgUqhQNkbwQ0DDIi0JrRgNC40L/RgtC+0J/RgNC+IENTUCA0LjAgUjQiICjQstC1
0YDRgdC40Y8gNC4wKTARBgkrBgEEAYI3FAIEBAwCQ0EwEgYJKwYBBAGCNxUBBAUC
AwIAAjAMBgUqhQNkcgQDAgEAMAoGCCqFAwcBAQMCA0EAg7WnK+Nxo9wQGY2ZtVGA
EP/KUJNPKWUYOOXk3FHJju/fntsd1kqcs3GkWEfFlrOtg309uRvLwaNc0C/AkEam
Ng==
-----END CERTIFICATE-----

View file

@ -101,3 +101,14 @@ fcgi_handler_map_get(fcgi_handler_map_t* map, char* path)
return handler;
}
void
fcgi_handler_map_clear(fcgi_handler_map_t* map)
{
if (map == NULL) return;
for (size_t i = 0; i < map->num; ++i) {
fcgi_map_item_clear(&map->locations[i]);
}
}

View file

@ -230,6 +230,15 @@ fcgi_register_handler(HFcgi hfcgi, char* path, fcgi_handler_t* handler)
}
static void
fcgi_unregister_handlers(HFcgi hfcgi)
{
if (hfcgi == NULL) return;
fcgi_handler_map_clear(&hfcgi->handler_map);
}
int
fcgi_run_server(HFcgi hfcgi)
{
@ -299,6 +308,7 @@ fcgi_dispose_server(HFcgi hfcgi)
{
LOG_TRACE("fcgi_dispose_server enter");
fcgi_unregister_handlers(hfcgi);
free(hfcgi);
LOG_TRACE("fcgi_dispose_server exit");

View file

@ -66,5 +66,6 @@ void fcgi_server_thread_pool_dispose(fcgi_thread_pool_t* thread_pool);
int fcgi_handler_map_set(fcgi_handler_map_t* map, char* path, fcgi_handler_t* handler);
fcgi_handler_t* fcgi_handler_map_get(fcgi_handler_map_t* map, char* path);
void fcgi_handler_map_clear(fcgi_handler_map_t* map);
#endif // FCGI_SERVER_INTERNAL_H_INCLUDED

View file

@ -17,6 +17,57 @@ fcgi_get_content_length(const FCGX_Request* request)
}
fcgi_handler_status_t
check_content_length(int content_length, int client_max_body_size)
{
LOG_TRACE("check_content_length");
if (content_length > 0 && content_length <= client_max_body_size) {
LOG_DEBUG("Content-Length is ok");
return HANDLER_SUCCESS;
}
if (content_length == 0) {
LOG_DEBUG("Content-Length == 0, so there is no body");
return HANDLER_HTTP_OK;
}
if (content_length < 0) {
LOG_WARN("Content-Length < 0 (%d)", content_length);
return HANDLER_HTTP_BAD_REQUEST;
}
/* (content_length > client_max_body_size) */
LOG_WARN("Content-Length (%d) is too large (> %d)",
content_length,
client_max_body_size);
return HANDLER_HTTP_REQUEST_ENTITY_TOO_LARGE;
}
fcgi_handler_status_t
check_content_type(const FCGX_Request* request, const char* acceptable_content_type)
{
LOG_TRACE("check_content_type");
const char* content_type = FCGX_GetParam(FCGI_PARAM_NAME_CONTENT_TYPE, request->envp);
if (content_type == NULL) {
LOG_ERROR("Could not get FCGI param: "FCGI_PARAM_NAME_CONTENT_TYPE);
return HANDLER_HTTP_BAD_REQUEST;
}
if (strcmp(content_type, acceptable_content_type) == 0) {
LOG_DEBUG("Content-Type is ok");
return HANDLER_SUCCESS;
}
LOG_WARN("Content-Type is not acceptable, '%s' (!= '%s')",
content_type, acceptable_content_type);
return HANDLER_HTTP_NOT_ACCEPTABLE;
}
char*
fcgi_request_get_param(const FCGX_Request* request, const char* name, /*out*/ bool* err)
{
@ -45,6 +96,40 @@ error:
}
fcgi_handler_status_t
fcgi_request_load_data(const FCGX_Request* request, int content_length, char** content)
{
LOG_TRACE("fcgi_request_load_data enter");
char* data = malloc(content_length + 1);
if (data == NULL) {
LOG_ERROR("Could not allocate memory for request body (%d bytes)", content_length + 1);
return HANDLER_ERROR;
}
int actual_content_length = FCGX_GetStr(data, content_length, request->in);
if (actual_content_length != content_length) {
LOG_ERROR("fcgi_request_load_data: "
"actual_content_length(%d) != content_length(%d); content: '%.*s'",
actual_content_length, content_length,
actual_content_length, data);
free(data);
return HANDLER_HTTP_BAD_REQUEST;
}
data[content_length] = '\0';
*content = data;
LOG_DEBUG("content_length: %d", content_length);
LOG_DEBUG("content: '%s'", *content);
LOG_TRACE("fcgi_request_load_data exit");
return HANDLER_SUCCESS;
}
fcgi_handler_status_t
fcgi_printf_str(const FCGX_Request* request, const char* response, int response_len)
{

View file

@ -56,6 +56,25 @@ char* fcgi_request_get_param(const FCGX_Request* request, const char* name, bool
int
fcgi_get_content_length(const FCGX_Request* request);
/**
* Returns: HANDLER_SUCCESS, HANDLER_HTTP_OK (no body), HANDLER_HTTP_BAD_REQUEST,
* HANDLER_HTTP_REQUEST_ENTITY_TOO_LARGE, HANDLER_ERROR
*/
fcgi_handler_status_t
check_content_length(int content_length, int client_max_body_size);
/**
* Returns: HANDLER_SUCCESS, HANDLER_HTTP_BAD_REQUEST, HANDLER_HTTP_NOT_ACCEPTABLE
*/
fcgi_handler_status_t
check_content_type(const FCGX_Request* request, const char* acceptable_content_type);
/**
* Returns: HANDLER_SUCCESS, HANDLER_HTTP_BAD_REQUEST, HANDLER_ERROR
*/
fcgi_handler_status_t
fcgi_request_load_data(const FCGX_Request* request, int content_length, char** content);
fcgi_handler_status_t
fcgi_printf_str(const FCGX_Request* request, const char* response, int response_len);
@ -70,6 +89,10 @@ fcgi_printf_header(const FCGX_Request* request, const char* name, const char* va
#define CRLF "\r\n"
#define FCGI_200_RESPONSE_FORMAT \
"Status: 200 OK" CRLF\
CRLF
#define FCGI_400_RESPONSE_FORMAT \
"Status: 400 Bad Request" CRLF\
"Content-type: text/plain" CRLF\
@ -123,6 +146,13 @@ typedef fcgi_handler_status_t (*fcgi_request_handler_pt)(const FCGX_Request* req
typedef fcgi_request_handler_pt (*FCGI_REQUEST_FINALIZE_HANDLER)(int rc);
static inline fcgi_handler_status_t
fcgi_200_ok_handler(const FCGX_Request* request, void* ctx __attribute__((unused)))
{
LOG_INFO("response status: '200 OK'");
return FCGI_CHECK_PRINTF_STATUS(request, FCGI_200_RESPONSE_FORMAT, 200);
}
static inline fcgi_handler_status_t
fcgi_400_bad_request_handler(const FCGX_Request* request, void* ctx __attribute__((unused)))
{

View file

@ -3,8 +3,9 @@
#include <assert.h>
#include <stdlib.h>
#define MAIN_CONF_SECTION "main"
#define MAIN_CONF_KEY_WORKER_PROCESSES "worker_processes"
#define MAIN_CONF_SECTION "main"
#define MAIN_CONF_KEY_WORKER_PROCESSES "worker_processes"
#define MAIN_CONF_KEY_CP_FILE "cp_file"
/* default configuration values: */
static const int FCGI_CONF_DEFAULT_WORKER_PROCESSES = 10;
@ -40,6 +41,14 @@ main_conf_load(main_conf_t* conf, const char *filename, const conf_file_context_
CONF_FILE_VALUE_POSITIVE_INT,
&FCGI_CONF_DEFAULT_WORKER_PROCESSES
},
{
MAIN_CONF_SECTION,
MAIN_CONF_KEY_CP_FILE,
&(conf->cp_file),
CONF_FILE_VALUE_STRING,
CONF_FILE_VALUE_NONE,
NULL
},
};
if (conf_file_load_values(conf_file, fields, sizeof(fields) / sizeof(conf_file_field_t))) {
@ -70,6 +79,7 @@ main_conf_clear(main_conf_t* conf)
return;
}
free(conf->cp_file);
free(conf->conf_file);
memset(conf, 0, sizeof(main_conf_t));
}

View file

@ -5,6 +5,7 @@
typedef struct main_conf_s {
int worker_processes;
char *cp_file; /* файл криптопровайдера */
char *conf_file;
} main_conf_t;

View file

@ -4,11 +4,10 @@
#include "utils/cryptopro.h"
#define SIGN_CONF_SECTION "sign"
#define SIGN_CONF_KEY_LOCATION "location"
#define SIGN_CONF_KEY_CP_FILE "cp_file"
#define SIGN_CONF_KEY_SIGNER "signer_subject"
#define SIGN_CONF_KEY_PIN "pin"
#define SIGN_CONF_SECTION "sign"
#define SIGN_CONF_KEY_LOCATION "location"
#define SIGN_CONF_KEY_SIGN_CERT_THUMBPRINT "sign_cert_thumbprint"
#define SIGN_CONF_KEY_SIGN_CERT_PASSWORD "sign_cert_password"
#define FCGI_OK_RESPONSE_FORMAT \
"Content-type: text/plain" CRLF\
@ -18,7 +17,7 @@
static const str_t SIGN_CONF_DEFAULT_LOCATION = str_t_const("/sign");
static const int CLIENT_MAX_BODY_SIZE = 4096;
static const str_t ACCEPTABLE_CONTENT_TYPE = str_t_const("text/plain");
static const char* ACCEPTABLE_CONTENT_TYPE = "text/plain";
typedef struct sign_service_s {
const sign_conf_t *conf;
@ -39,13 +38,6 @@ static void fcgi_sign_request_clear(fcgi_sign_request_t *req_info);
static fcgi_request_handler_pt fcgi_request_finalize_handler(fcgi_handler_status_t status);
static fcgi_handler_status_t check_content_length(int content_length);
static fcgi_handler_status_t check_content_type(const FCGX_Request* request);
static fcgi_handler_status_t fcgi_request_load_data(const FCGX_Request* request,
int content_length, char** content);
static int sign_content(const sign_service_t *hsign, fcgi_sign_request_t *req_info);
int
@ -64,26 +56,18 @@ sign_conf_load(sign_conf_t *conf, const conf_file_context_t conf_file)
CONF_FILE_VALUE_LOCATION,
&SIGN_CONF_DEFAULT_LOCATION
},
{
SIGN_CONF_SECTION,
SIGN_CONF_KEY_CP_FILE,
&(conf->cp_file),
CONF_FILE_VALUE_STRING,
CONF_FILE_VALUE_NONE,
NULL
},
{
SIGN_CONF_SECTION,
SIGN_CONF_KEY_SIGNER,
&(conf->signer),
CONF_FILE_VALUE_STRING,
SIGN_CONF_KEY_SIGN_CERT_THUMBPRINT,
&(conf->sign_cert_thumbprint),
CONF_FILE_VALUE_STRT,
CONF_FILE_VALUE_NONE,
NULL
},
{
SIGN_CONF_SECTION,
SIGN_CONF_KEY_PIN,
&(conf->pin),
SIGN_CONF_KEY_SIGN_CERT_PASSWORD,
&(conf->sign_cert_password),
CONF_FILE_VALUE_STRT,
CONF_FILE_VALUE_PSWD,
NULL
@ -110,14 +94,13 @@ sign_conf_clear(sign_conf_t *conf)
if (conf == NULL) return;
if (conf->pin.data != 0) {
explicit_bzero(conf->pin.data, conf->pin.len);
if (conf->sign_cert_password.data != 0) {
explicit_bzero(conf->sign_cert_password.data, conf->sign_cert_password.len);
}
str_t_clear(&conf->pin);
str_t_clear(&conf->sign_cert_password);
str_t_clear(&conf->location);
free(conf->cp_file);
free(conf->signer);
str_t_clear(&conf->sign_cert_thumbprint);
memset(conf, 0, sizeof(sign_conf_t));
}
@ -135,12 +118,9 @@ sign_service_create(const sign_conf_t *conf)
hsign->conf = conf;
if (!cryptopro_init(conf->cp_file)) {
LOG_ERROR("Could not init Cryptographic Provider");
goto error;
}
cryptopro_context_set(&hsign->cryptopro_ctx, conf->signer, &conf->pin);
cryptopro_context_set(&hsign->cryptopro_ctx,
&conf->sign_cert_thumbprint,
&conf->sign_cert_password);
LOG_TRACE("sign_service_create exit");
return (HSign)hsign;
@ -177,12 +157,12 @@ fcgi_sign_handler(FCGX_Request* request, void* ctx)
req_info.content_length = fcgi_get_content_length(request);
status = check_content_length(req_info.content_length);
status = check_content_length(req_info.content_length, CLIENT_MAX_BODY_SIZE);
if (status != HANDLER_SUCCESS) {
goto exit;
}
status = check_content_type(request);
status = check_content_type(request, ACCEPTABLE_CONTENT_TYPE);
if (status != HANDLER_SUCCESS) {
goto exit;
}
@ -268,90 +248,6 @@ fcgi_request_finalize_handler(fcgi_handler_status_t status)
return handler;
}
static fcgi_handler_status_t
check_content_length(int content_length)
{
LOG_TRACE("check_content_length");
if (content_length > 0 && content_length <= CLIENT_MAX_BODY_SIZE) {
LOG_DEBUG("Content-Length is ok");
return HANDLER_SUCCESS;
}
if (content_length == 0) {
LOG_DEBUG("Content-Length == 0, so there is nothing to sign");
return HANDLER_HTTP_OK;
}
if (content_length < 0) {
LOG_WARN("Content-Length < 0 (%d)", content_length);
return HANDLER_HTTP_BAD_REQUEST;
}
if (content_length > CLIENT_MAX_BODY_SIZE) {
LOG_WARN("Content-Length (%d) is too large (> %d)",
content_length,
CLIENT_MAX_BODY_SIZE);
return HANDLER_HTTP_REQUEST_ENTITY_TOO_LARGE;
}
return HANDLER_ERROR;
}
static fcgi_handler_status_t
check_content_type(const FCGX_Request* request)
{
LOG_TRACE("check_content_type");
const char* content_type = FCGX_GetParam(FCGI_PARAM_NAME_CONTENT_TYPE, request->envp);
if (content_type == NULL) {
LOG_ERROR("Could not get FCGI param: "FCGI_PARAM_NAME_CONTENT_TYPE);
return HANDLER_HTTP_BAD_REQUEST;
}
if (strncmp(content_type, ACCEPTABLE_CONTENT_TYPE.data, ACCEPTABLE_CONTENT_TYPE.len) == 0) {
LOG_DEBUG("Content-Type is ok");
return HANDLER_SUCCESS;
}
LOG_WARN("Content-Type is not acceptable, '%s'", content_type);
return HANDLER_HTTP_NOT_ACCEPTABLE;
}
static fcgi_handler_status_t
fcgi_request_load_data(const FCGX_Request* request, int content_length, char** content)
{
LOG_TRACE("fcgi_request_load_data enter");
char* data = malloc(content_length + 1);
if (data == NULL) {
LOG_ERROR("Could not allocate memory for request body (%d bytes)", content_length + 1);
return HANDLER_ERROR;
}
int actual_content_length = FCGX_GetStr(data, content_length, request->in);
if (actual_content_length != content_length) {
LOG_ERROR("fcgi_request_load_data: "
"actual_content_length(%d) != content_length(%d); content: '%.*s'",
actual_content_length, content_length,
actual_content_length, data);
free(data);
return HANDLER_HTTP_BAD_REQUEST;
}
data[content_length] = '\0';
*content = data;
LOG_DEBUG("content_length: %d", content_length);
LOG_DEBUG("content: '%s'", *content);
LOG_TRACE("fcgi_request_load_data exit");
return HANDLER_SUCCESS;
}
static int
sign_content(const sign_service_t *hsign, fcgi_sign_request_t *req_info)
{

View file

@ -11,10 +11,8 @@ typedef struct sign_service_t* HSign;
typedef struct sign_conf_s {
str_t location;
char *cp_file; /* файл криптопровайдера */
char *signer; /* субъект контейнера */
str_t pin; /* pin-код от контейнера */
str_t sign_cert_thumbprint; /* SHA1 отпечаток сертификата ИС */
str_t sign_cert_password; /* пароль от контейнера */
} sign_conf_t;

View file

@ -0,0 +1,307 @@
#include "service_verify.h"
#include "fcgisrv/fcgi_utils.h"
#include "utils/base64.h"
#include "utils/cryptopro.h"
#include "utils/jwt.h"
#define VERIFY_CONF_SECTION "verify"
#define VERIFY_CONF_KEY_LOCATION "location"
#define VERIFY_CONF_KEY_THUMBPRINT "esia_cert_thumbprint"
#define FCGI_401_BAD_SIGNATURE_RESPONSE_FORMAT \
"Status: 401 Unauthorized" CRLF\
"Content-type: text/plain" CRLF\
CRLF\
"%s" CRLF
static const str_t VERIFY_CONF_DEFAULT_LOCATION = str_t_const("/verify");
static const int CLIENT_MAX_BODY_SIZE = 8192;
static const char* ACCEPTABLE_CONTENT_TYPE = "text/plain";
typedef struct verify_service_s {
const verify_conf_t *conf;
} verify_service_t;
typedef struct fcgi_verify_request_s {
char *content;
int content_length;
char *verify_error;
} fcgi_verify_request_t;
static fcgi_request_handler_pt fcgi_request_finalize_handler(fcgi_handler_status_t status);
static void fcgi_verify_request_clear(fcgi_verify_request_t *req_info);
static fcgi_handler_status_t verify_jwt_sign(fcgi_verify_request_t* req_info,
const verify_service_t *ctx);
int
verify_conf_load(verify_conf_t *conf, const conf_file_context_t conf_file)
{
LOG_TRACE("verify_conf_load enter");
memset(conf, 0, sizeof(verify_conf_t));
conf_file_field_t fields[] = {
{
VERIFY_CONF_SECTION,
VERIFY_CONF_KEY_LOCATION,
&conf->location,
CONF_FILE_VALUE_STRT,
CONF_FILE_VALUE_LOCATION,
&VERIFY_CONF_DEFAULT_LOCATION
},
{
VERIFY_CONF_SECTION,
VERIFY_CONF_KEY_THUMBPRINT,
&conf->esia_cert_thumbprint,
CONF_FILE_VALUE_STRT,
CONF_FILE_VALUE_NONE,
NULL
},
};
if (conf_file_load_values(conf_file, fields, sizeof(fields) / sizeof(conf_file_field_t))) {
goto error;
}
LOG_TRACE("verify_conf_load exit");
return 0;
error:
verify_conf_clear(conf);
LOG_ERROR("verify_conf_load exit with error");
return -1;
}
void
verify_conf_clear(verify_conf_t *conf)
{
LOG_TRACE("verify_conf_clear");
if (conf == NULL) return;
str_t_clear(&conf->location);
str_t_clear(&conf->esia_cert_thumbprint);
memset(conf, 0, sizeof(verify_conf_t));
}
HVerify
verify_service_create(const verify_conf_t *conf)
{
LOG_TRACE("verify_service_create enter");
verify_service_t *hverify = (verify_service_t*) calloc(1, sizeof(verify_service_t));
if (hverify == NULL) {
LOG_ERROR("Could not allocate memory for HVerify");
goto error;
}
hverify->conf = conf;
LOG_TRACE("verify_service_create exit");
return (HVerify)hverify;
error:
LOG_ERROR("verify_service_create exit with error");
return NULL;
}
void
verify_service_free(HVerify hverify)
{
LOG_TRACE("verify_service_free");
verify_service_t *ctx = (verify_service_t *) hverify;
if (ctx == NULL) return;
free(ctx);
}
fcgi_handler_status_t
fcgi_verify_handler(FCGX_Request* request, void* ctx)
{
fcgi_handler_status_t status = HANDLER_ERROR;
fcgi_verify_request_t req_info = {0};
LOG_TRACE("fcgi_verify_handler enter");
if (request == NULL) {
LOG_ERROR("fcgi_verify_handler exit with error. Desc: request is NULL");
return HANDLER_ERROR;
}
req_info.content_length = fcgi_get_content_length(request);
status = check_content_length(req_info.content_length, CLIENT_MAX_BODY_SIZE);
if (status != HANDLER_SUCCESS) {
goto exit;
}
status = check_content_type(request, ACCEPTABLE_CONTENT_TYPE);
if (status != HANDLER_SUCCESS) {
goto exit;
}
status = fcgi_request_load_data(request, req_info.content_length, &req_info.content);
if (status != HANDLER_SUCCESS) {
goto exit;
}
status = verify_jwt_sign(&req_info, ctx);
exit:
status = fcgi_request_finalize_handler(status)(request, &req_info);
fcgi_verify_request_clear(&req_info);
LOG_TRACE("fcgi_verify_handler exit");
return status;
}
static fcgi_handler_status_t
fcgi_401_bad_signature_handler(const FCGX_Request* request, void *ctx)
{
LOG_TRACE("fcgi_401_bad_signature_handler");
const fcgi_verify_request_t *req_info = (fcgi_verify_request_t*) ctx;
assert(req_info->verify_error != NULL);
LOG_DEBUG("response status: " FCGI_401_BAD_SIGNATURE_RESPONSE_FORMAT,
req_info->verify_error);
if (FCGX_FPrintF(request->out, FCGI_401_BAD_SIGNATURE_RESPONSE_FORMAT,
req_info->verify_error) < 0) {
LOG_ERROR("FCGX_FPrintF() failed");
return HANDLER_ERROR;
}
return HANDLER_SUCCESS;
}
static fcgi_request_handler_pt
fcgi_request_finalize_handler(fcgi_handler_status_t status)
{
fcgi_request_handler_pt handler;
switch (status) {
case HANDLER_SUCCESS:
case HANDLER_HTTP_OK:
handler = fcgi_200_ok_handler;
break;
case HANDLER_HTTP_BAD_REQUEST:
handler = fcgi_400_bad_request_handler;
break;
case HANDLER_HTTP_UNAUTHORIZED:
handler = fcgi_401_bad_signature_handler;
break;
case HANDLER_HTTP_NOT_ACCEPTABLE:
handler = fcgi_406_not_acceptable_handler;
break;
case HANDLER_HTTP_REQUEST_ENTITY_TOO_LARGE:
handler = fcgi_413_request_entity_too_large_handler;
break;
case HANDLER_ERROR:
default:
handler = fcgi_500_internal_server_error_handler;
break;
}
return handler;
}
static void
fcgi_verify_request_clear(fcgi_verify_request_t *req_info)
{
LOG_TRACE("fcgi_verify_request_clear");
free(req_info->content);
free(req_info->verify_error);
memset(req_info, 0, sizeof(fcgi_verify_request_t));
}
static fcgi_handler_status_t
verify_jwt_sign(fcgi_verify_request_t* req_info, const verify_service_t *ctx)
{
LOG_TRACE("verify_jwt_sign enter");
bool is_verified = false;
str_t jwt = {
.data = req_info->content,
.len = req_info->content_length
};
str_t sign = str_t_null;
str_t sign_base64 = str_t_null;
str_t header = str_t_null;
str_t header_base64 = str_t_null;
str_t header_payload = str_t_null;
str_t alg = str_t_null;
if (jwt_get_header_payload_and_sign(&jwt, &header_base64, &header_payload, &sign_base64)) {
goto error;
}
sign.len = base64_decoded_length(sign_base64.len);
sign.data = calloc(1, sign.len + 1);
if (sign.data == NULL) {
LOG_ERROR("Could not allocate memory for decoded sign (%zd bytes)", sign.len + 1);
goto error;
}
if (decode_base64_url(&sign, &sign_base64)) {
LOG_ERROR("Could not decode jwt sign");
goto error;
}
header.len = base64_decoded_length(header_base64.len);
header.data = calloc(1, header.len + 1);
if (header.data == NULL) {
LOG_ERROR("Could not allocate memory for decoded header (%zd bytes)", header.len + 1);
goto error;
}
if (decode_base64_url(&header, &header_base64)) {
LOG_ERROR("Could not decode jwt header");
goto error;
}
if (jwt_get_alg_from_header(&header, &alg)) {
goto error;
}
if (cryptopro_verify(&ctx->conf->esia_cert_thumbprint, &alg, &header_payload, &sign,
&is_verified, &req_info->verify_error)) {
goto error;
}
str_t_clear(&sign);
str_t_clear(&header);
str_t_clear(&alg);
LOG_TRACE("verify_jwt_sign exit");
return is_verified ? HANDLER_SUCCESS : HANDLER_HTTP_UNAUTHORIZED;
error:
str_t_clear(&sign);
str_t_clear(&header);
str_t_clear(&alg);
LOG_TRACE("verify_jwt_sign exit with error");
return HANDLER_ERROR;
}

View file

@ -0,0 +1,25 @@
#ifndef SERVICE_VERIFY_H_INCLUDED
#define SERVICE_VERIFY_H_INCLUDED
#include "fcgisrv/fcgi_server.h"
#include "utils/conf_file_context.h"
#include "utils/str_t.h"
typedef struct verify_service_t* HVerify;
typedef struct verify_conf_s {
str_t location;
str_t esia_cert_thumbprint;
} verify_conf_t;
int verify_conf_load(verify_conf_t *conf, const conf_file_context_t conf_file);
void verify_conf_clear(verify_conf_t *conf);
HVerify verify_service_create(const verify_conf_t *conf);
void verify_service_free(HVerify hverify);
fcgi_handler_status_t fcgi_verify_handler(FCGX_Request* request, void* ctx);
#endif // SERVICE_VERIFY_H_INCLUDED

View file

@ -0,0 +1,58 @@
#include "service_version.h"
#include "version.h"
#include "fcgisrv/fcgi_utils.h"
#include "utils/logger.h"
#include <assert.h>
#define VERSION_TEXT \
"Content-type: text/plain" CRLF CRLF \
APP_VERSION CRLF
static const str_t VERSION_CONF_DEFAULT_LOCATION = str_t_const("/version");
int
version_conf_load(version_conf_t *conf)
{
LOG_TRACE("version_conf_load enter");
assert(conf != NULL);
conf->location = &VERSION_CONF_DEFAULT_LOCATION;
LOG_TRACE("version_conf_load exit");
return 0;
}
void
version_conf_clear(version_conf_t *conf)
{
LOG_TRACE("version_conf_clear enter");
if (conf != NULL) {
conf->location = NULL;
}
LOG_TRACE("version_conf_clear exit");
}
fcgi_handler_status_t
fcgi_version_handler(FCGX_Request* request, void* ctx)
{
LOG_TRACE("fcgi_version_handler enter");
if (request == NULL) {
LOG_ERROR("fcgi_version_handler failed: request is NULL");
return HANDLER_ERROR;
}
FCGX_FPrintF(request->out, VERSION_TEXT);
LOG_TRACE("fcgi_version_handler exit");
return HANDLER_SUCCESS;
}

View file

@ -0,0 +1,22 @@
#ifndef SERVICE_VERSION_H_INCLUDED
#define SERVICE_VERSION_H_INCLUDED
#include "fcgisrv/fcgi_server.h"
#include "utils/str_t.h"
typedef struct version_conf_s {
const str_t *location;
} version_conf_t;
int version_conf_load(version_conf_t *conf);
void version_conf_clear(version_conf_t *conf);
fcgi_handler_status_t fcgi_version_handler(FCGX_Request* request, void* ctx);
#endif // SERVICE_VERSION_H_INCLUDED

View file

@ -1,5 +1,7 @@
#include "service_manager.h"
#include "utils/cryptopro.h"
#include <assert.h>
@ -158,6 +160,16 @@ service_manager_load_conf(const char* config_file_name, service_manager_conf_t*
LOG_ERROR("Sign service configuraton loading failed");
goto error;
}
if (verify_conf_load(&services_cf->verify_cf, conf_file)) {
LOG_ERROR("Verify service configuraton loading failed");
goto error;
}
if (version_conf_load(&services_cf->version_cf)) {
LOG_ERROR("Show version service configuraton loading failed");
goto error;
}
conf_file_close_file(conf_file);
@ -178,7 +190,9 @@ service_manager_clear_conf(service_manager_conf_t* services_cf)
{
assert(services_cf != NULL);
version_conf_clear(&services_cf->version_cf);
sign_conf_clear(&services_cf->sign_cf);
verify_conf_clear(&services_cf->verify_cf);
fcgi_clear_conf(&services_cf->fcgi_cf);
main_conf_clear(&services_cf->main_cf);
}
@ -196,6 +210,8 @@ deinit_services(service_manager_t* services)
sign_service_free(services->hsign);
verify_service_free(services->hverify);
LOG_TRACE("deinit_services exit");
return 0;
@ -217,6 +233,12 @@ init_services(service_manager_t* services, const service_manager_conf_t* service
goto error;
}
if (!cryptopro_init(services_cf->main_cf.cp_file)) {
LOG_ERROR("Could not init Cryptographic Provider");
goto error;
}
/* sign service */
services->hsign = sign_service_create(&services_cf->sign_cf);
if (services->hsign == NULL) {
goto error;
@ -227,6 +249,25 @@ init_services(service_manager_t* services, const service_manager_conf_t* service
LOG_ERROR("Could not register 'sign service'");
goto error;
}
/* verify service */
services->hverify = verify_service_create(&services_cf->verify_cf);
if (services->hverify == NULL) {
goto error;
}
if (register_service(services, services_cf, fcgi_verify_handler, services->hverify,
&services_cf->verify_cf.location)) {
LOG_ERROR("Could not register 'verify service'");
goto error;
}
/* show version service */
if (register_service(services, services_cf, fcgi_version_handler, NULL,
services_cf->version_cf.location)) {
LOG_ERROR("Could not register 'show version service'");
goto error;
}
/* run fastcgi server */
if (fcgi_run_server(services->hfcgi) != 0) {

View file

@ -8,6 +8,8 @@
#include "fcgisrv/fcgi_server.h"
#include "modules/service_sign.h"
#include "modules/service_verify.h"
#include "modules/service_version.h"
struct service_s;
@ -46,12 +48,15 @@ typedef struct service_manager_conf_s {
fcgi_conf_t fcgi_cf;
main_conf_t main_cf;
sign_conf_t sign_cf;
verify_conf_t verify_cf;
version_conf_t version_cf;
} service_manager_conf_t;
typedef struct service_manager_s {
HFcgi hfcgi;
HSign hsign;
HVerify hverify;
} service_manager_t;

View file

@ -1,5 +1,7 @@
#include "base64.h"
#include "logger.h"
#include <assert.h>
#include <stdint.h>
@ -79,4 +81,89 @@ encode_base64_url(str_t *dst, const str_t *src)
dst->data[n * 4 + 3] = basis64_url[d];
}
}
}
}
static void
init_decode_base64_url(unsigned char **table)
{
static unsigned char basis64_url[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0xFF, 0xFF,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F,
0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
assert(table != NULL);
*table = basis64_url;
}
int
decode_base64_url(str_t *dst, const str_t *src)
{
uint32_t value = 0;
unsigned int c;
size_t i;
size_t n = 0;
uint8_t *p = NULL;
assert(dst != NULL && !str_t_is_null(*dst));
assert(src != NULL && !str_t_is_null(*src));
if ((src->len % 4) == 1) {
LOG_ERROR("decode_base64_url failed. Desc: invalid src length (%zd)", src->len);
goto error;
}
unsigned char *basis64_url;
init_decode_base64_url(&basis64_url);
p = (uint8_t *) dst->data;
for (i = 0; i < src->len; i++) {
c = (unsigned int) src->data[i];
if (c < 128 && basis64_url[c] < 64) {
value = (value << 6) | basis64_url[c];
if ((i % 4) == 3) {
if (p != NULL) {
p[n] = (value >> 16) & 0xFF;
p[n + 1] = (value >> 8) & 0xFF;
p[n + 2] = value & 0xFF;
}
n += 3;
value = 0;
}
} else {
LOG_ERROR("decode_base64_url failed. Desc: invalid character: %c", src->data[i]);
goto error;
}
}
if ((src->len % 4) == 2) {
if (p != NULL) {
p[n] = (value >> 4) & 0xFF;
}
n++;
} else if ((src->len % 4) == 3) {
if (p != NULL) {
p[n] = (value >> 10) & 0xFF;
p[n + 1] = (value >> 2) & 0xFF;
}
n += 2;
}
dst->len = n;
return 0;
error:
LOG_ERROR("decode_base64_url exit with error");
return -1;
}

View file

@ -4,7 +4,9 @@
#include "str_t.h"
#define base64_encoded_length(len) (((len + 2) / 3) * 4)
#define base64_decoded_length(len) (((len + 3) / 4) * 3)
void encode_base64_url(str_t *dst, const str_t *src);
int decode_base64_url(str_t *dst, const str_t *src);
#endif // BASE64_H

View file

@ -26,11 +26,17 @@ capi_function_list_init(library_t *lib, capi_function_list_t *fl)
LIBRARY_RESOLVE(fl->CryptReleaseContext, lib, "CryptReleaseContext");
LIBRARY_RESOLVE(fl->CryptSignMessage, lib, "CryptSignMessage");
LIBRARY_RESOLVE(fl->GetLastError, lib, "GetLastError");
LIBRARY_RESOLVE(fl->CryptImportPublicKeyInfo, lib, "CryptImportPublicKeyInfo");
LIBRARY_RESOLVE(fl->CryptDestroyKey, lib, "CryptDestroyKey");
#ifdef UNICODE
LIBRARY_RESOLVE(fl->CryptSignHash, lib, "CryptSignHashW");
LIBRARY_RESOLVE(fl->CryptVerifySignature, lib, "CryptVerifySignatureW");
LIBRARY_RESOLVE(fl->CryptAcquireContext, lib, "CryptAcquireContextW");
#else
LIBRARY_RESOLVE(fl->CryptSignHash, lib, "CryptSignHashA");
LIBRARY_RESOLVE(fl->CryptVerifySignature, lib, "CryptVerifySignatureA");
LIBRARY_RESOLVE(fl->CryptAcquireContext, lib, "CryptAcquireContextA");
#endif // !UNICODE
return true;

View file

@ -124,10 +124,65 @@ DECLARE_FN(WINADVAPI,
BYTE *pbSignature,
DWORD *pdwSigLen));
DECLARE_FN(WINADVAPI,
BOOL,
CRYPT_VERIFY_SIGNATURE_A,
(HCRYPTHASH hHash,
CONST BYTE *pbSignature,
DWORD dwSigLen,
HCRYPTKEY hPubKey,
LPCSTR szDescription,
DWORD dwFlags));
DECLARE_FN(WINADVAPI,
BOOL,
CRYPT_VERIFY_SIGNATURE_W,
(HCRYPTHASH hHash,
CONST BYTE *pbSignature,
DWORD dwSigLen,
HCRYPTKEY hPubKey,
LPCWSTR szDescription,
DWORD dwFlags));
DECLARE_FN(WINADVAPI,
BOOL,
CRYPT_ACQUIRE_CONTEXT_A,
(HCRYPTPROV *phProv,
LPCSTR pszContainer,
LPCSTR pszProvider,
DWORD dwProvType,
DWORD dwFlags));
DECLARE_FN(WINADVAPI,
BOOL,
CRYPT_ACQUIRE_CONTEXT_W,
(HCRYPTPROV *phProv,
LPCSTR pszContainer,
LPCSTR pszProvider,
DWORD dwProvType,
DWORD dwFlags));
DECLARE_FN(WINADVAPI,
BOOL,
CRYPT_IMPORT_PUBLIC_KEY_INFO,
(HCRYPTPROV hCryptProv,
DWORD dwCertEncodingType,
PCERT_PUBLIC_KEY_INFO pInfo,
HCRYPTKEY *phKey));
DECLARE_FN(WINADVAPI,
BOOL,
CRYPT_DESTROY_KEY,
(HCRYPTKEY hKey));
#ifdef UNICODE
#define CRYPT_SIGN_HASH_FN CRYPT_SIGN_HASH_W_FN
#define CRYPT_VERIFY_SIGNATURE_FN CRYPT_VERIFY_SIGNATURE_W_FN
#define CRYPT_ACQUIRE_CONTEXT_FN CRYPT_ACQUIRE_CONTEXT_W_FN
#else
#define CRYPT_SIGN_HASH_FN CRYPT_SIGN_HASH_A_FN
#define CRYPT_VERIFY_SIGNATURE_FN CRYPT_VERIFY_SIGNATURE_A_FN
#define CRYPT_ACQUIRE_CONTEXT_FN CRYPT_ACQUIRE_CONTEXT_A_FN
#endif // !UNICODE
@ -146,6 +201,11 @@ typedef struct {
CERT_FIND_CERTIFICATE_IN_STORE_FN CertFindCertificateInStore;
CERT_GET_CERTIFICATE_CONTEXT_PROPERTY_FN CertGetCertificateContextProperty;
CERT_SET_CERTIFICATE_CONTEXT_PROPERTY_FN CertSetCertificateContextProperty;
CRYPT_VERIFY_SIGNATURE_FN CryptVerifySignature;
CRYPT_ACQUIRE_CONTEXT_FN CryptAcquireContext;
CRYPT_IMPORT_PUBLIC_KEY_INFO_FN CryptImportPublicKeyInfo;
CRYPT_DESTROY_KEY_FN CryptDestroyKey;
} capi_function_list_t;

View file

@ -16,7 +16,7 @@ static HCERTSTORE cert_open_store();
static PCCERT_CONTEXT get_signer_cert(const cryptopro_context_t *ctx, HCERTSTORE hStoreHandle);
static int set_pin(PCCERT_CONTEXT pSignerCert, const str_t *pin);
static int set_password(PCCERT_CONTEXT cert_ctx, const str_t *password);
bool
cryptopro_init(const char* cp_file)
@ -44,8 +44,8 @@ cryptopro_sign(const cryptopro_context_t *ctx, const str_t *data, /*out*/ str_t
LOG_TRACE("cryptopro_sign enter");
assert(ctx != NULL);
assert(ctx->signer != NULL);
assert(ctx->pin != NULL);
assert(!str_t_is_null(*ctx->cert_thumbprint));
assert(ctx->password != NULL);
assert(data != NULL && !str_t_is_null(*data));
assert(sign != NULL);
@ -209,47 +209,78 @@ cert_open_store()
}
static PCCERT_CONTEXT
get_signer_cert(const cryptopro_context_t *ctx, HCERTSTORE hStoreHandle)
get_cert_by_thumbprint(HCERTSTORE hStoreHandle, const str_t* thumbprint)
{
PCCERT_CONTEXT pSignerCert;
str_t thumbprint_bin = str_t_null;
pSignerCert = cp_function_list.CertFindCertificateInStore(hStoreHandle,
LOG_TRACE("get_cert_by_thumbprint enter");
if (hex_to_bin(thumbprint, &thumbprint_bin)) {
goto error;
}
CRYPT_HASH_BLOB hash = {
.pbData = (BYTE*)thumbprint_bin.data,
.cbData = thumbprint_bin.len
};
PCCERT_CONTEXT cert = cp_function_list.CertFindCertificateInStore(hStoreHandle,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_SUBJECT_STR,
ctx->signer,
CERT_FIND_HASH,
&hash,
NULL);
if (pSignerCert == NULL) {
if (cert == NULL) {
LOG_ERROR("Could not find certificate in store");
goto error;
}
if (set_pin(pSignerCert, ctx->pin)) {
str_t_clear(&thumbprint_bin);
LOG_TRACE("get_cert_by_thumbprint exit");
return cert;
error:
str_t_clear(&thumbprint_bin);
LOG_ERROR("get_cert_by_thumbprint exit with error");
return NULL;
}
static PCCERT_CONTEXT
get_signer_cert(const cryptopro_context_t *ctx, HCERTSTORE hStoreHandle)
{
PCCERT_CONTEXT cert_ctx = get_cert_by_thumbprint(hStoreHandle, ctx->cert_thumbprint);
if (cert_ctx == NULL) {
goto error;
}
if (set_password(cert_ctx, ctx->password)) {
goto error;
}
LOG_DEBUG("The signer's certificate was found");
return pSignerCert;
return cert_ctx;
error:
if (pSignerCert) {
cp_function_list.CertFreeCertificateContext(pSignerCert);
if (cert_ctx) {
cp_function_list.CertFreeCertificateContext(cert_ctx);
}
LOG_ERROR("get_signer_cert exit with error");
return NULL;
}
static int
set_pin(PCCERT_CONTEXT pSignerCert, const str_t *pin)
set_password(PCCERT_CONTEXT cert_ctx, const str_t *password)
{
DWORD dwSize;
if (!cp_function_list.CertGetCertificateContextProperty(
pSignerCert,
cert_ctx,
CERT_KEY_PROV_INFO_PROP_ID,
NULL,
&dwSize)) {
LOG_ERROR("Error getting key property");
LOG_ERROR("set_pin exit with error. Last error code: 0x%08x", cp_function_list.GetLastError());
LOG_ERROR("set_password exit with error. Last error code: 0x%08x", cp_function_list.GetLastError());
return -1;
}
@ -257,7 +288,7 @@ set_pin(PCCERT_CONTEXT pSignerCert, const str_t *pin)
CRYPT_KEY_PROV_INFO* pKeyInfo = (CRYPT_KEY_PROV_INFO*) pKeyInfoBuffer;
if (!cp_function_list.CertGetCertificateContextProperty(
pSignerCert,
cert_ctx,
CERT_KEY_PROV_INFO_PROP_ID,
pKeyInfo,
&dwSize)){
@ -272,14 +303,14 @@ set_pin(PCCERT_CONTEXT pSignerCert, const str_t *pin)
memset(&keyProvParam, 0, sizeof(CRYPT_KEY_PROV_PARAM));
keyProvParam.dwFlags = 0;
keyProvParam.dwParam = keyType;
keyProvParam.cbData = (DWORD)(pin->len + 1);
keyProvParam.pbData = (BYTE*) pin->data;
keyProvParam.cbData = (DWORD)(password->len + 1);
keyProvParam.pbData = (BYTE*) password->data;
pKeyInfo->cProvParam = 1;
pKeyInfo->rgProvParam = &keyProvParam;
if (!cp_function_list.CertSetCertificateContextProperty(
pSignerCert,
cert_ctx,
CERT_KEY_PROV_INFO_PROP_ID,
0,
pKeyInfo)) {
@ -290,6 +321,182 @@ set_pin(PCCERT_CONTEXT pSignerCert, const str_t *pin)
return 0;
error:
LOG_ERROR("set_pin exit with error. Last error code: 0x%08x", cp_function_list.GetLastError());
LOG_ERROR("set_password exit with error. Last error code: 0x%08x", cp_function_list.GetLastError());
return -1;
}
}
static int
alg_id_from_str(const str_t* alg, /*out*/ ALG_ID* alg_id)
{
LOG_TRACE("alg_id_from_str enter");
if (strncmp(alg->data, "GOST3410_2012_256", alg->len) == 0) {
*alg_id = CALG_GR3411_2012_256;
} else if (strncmp(alg->data, "GOST3410_2012_512", alg->len) == 0) {
*alg_id = CALG_GR3411_2012_512;
} else if (strncmp(alg->data, "RS256", alg->len) == 0) {
*alg_id = CALG_SHA_256;
} else {
LOG_ERROR("Unknown alg: '%.*s'", (int) alg->len, alg->data);
LOG_ERROR("alg_id_from_str exit with error");
return -1;
}
LOG_TRACE("alg_id_from_str exit");
return 0;
}
static void
get_verify_error(char** verify_error)
{
LOG_TRACE("get_verify_error enter");
DWORD err = cp_function_list.GetLastError();
const char* err_string;
switch (err) {
case NTE_BAD_SIGNATURE:
err_string = "sign is invalid: bad signature (0x%08x)";
break;
case NTE_BAD_ALGID:
err_string = "sign is invalid: bad alg id (0x%08x)";
break;
default:
err_string = "sign is invalid. Last error code: 0x%08x";
break;
}
size_t size = strlen(err_string) + 4 /*error code*/ + 1 /*terminating null*/;
*verify_error = malloc(size);
if (*verify_error == NULL) {
LOG_ERROR("get_verify_error failed. Could not allocate memory for error string "
"(%zd bytes)", size);
return;
}
int n = snprintf(*verify_error, size, err_string, err);
if (n < 0 || (size_t)n >= size) {
LOG_ERROR("get_verify_error failed. Error occurred in concatenation err_string");
free(*verify_error);
*verify_error = NULL;
return;
}
LOG_TRACE("get_verify_error exit");
}
int
cryptopro_verify(const str_t* cert_thumbprint, const str_t* alg, const str_t* data,
const str_t* sign, bool* is_verified, char** verify_error)
{
int rc = -1;
HCERTSTORE hStoreHandle = NULL;
PCCERT_CONTEXT certificate = NULL;
HCRYPTPROV hCryptProv = 0;
HCRYPTHASH hash = 0;
HCRYPTKEY hPubKey = 0;
str_t sign_reversed = str_t_null;
ALG_ID alg_id;
LOG_TRACE("cryptopro_verify enter");
*is_verified = false;
if (alg_id_from_str(alg, &alg_id)) {
goto exit;
}
if (reverse_sign(sign, &sign_reversed)) {
goto exit;
}
hStoreHandle = cert_open_store();
if (hStoreHandle == NULL) {
goto exit;
}
certificate = get_cert_by_thumbprint(hStoreHandle, cert_thumbprint);
if (certificate == NULL) {
goto exit;
}
if (!cp_function_list.CryptAcquireContext(&hCryptProv, NULL, CP_KC1_GR3410_2001_PROV,
PROV_GOST_2001_DH, CRYPT_VERIFYCONTEXT)) {
LOG_ERROR("CryptAcquireContext() failed");
goto exit;
}
if (!cp_function_list.CryptImportPublicKeyInfo(hCryptProv, X509_ASN_ENCODING,
&certificate->pCertInfo->SubjectPublicKeyInfo,
&hPubKey)) {
LOG_ERROR("CryptImportPublicKeyInfo() failed");
goto exit;
}
if (!cp_function_list.CryptCreateHash(hCryptProv, alg_id, 0, 0, &hash)) {
LOG_ERROR("CryptCreateHash() failed");
goto exit;
}
if (!cp_function_list.CryptHashData(hash, (CONST BYTE *) data->data, data->len, 0)) {
LOG_ERROR("CryptHashData() failed");
goto exit;
}
if (cp_function_list.CryptVerifySignature(hash,
(CONST BYTE *) sign_reversed.data,
sign_reversed.len,
hPubKey,
NULL,
0)) {
LOG_DEBUG("sign is valid");
*is_verified = true;
} else {
get_verify_error(verify_error);
if (*verify_error == NULL) {
goto exit;
}
LOG_WARN("%s", *verify_error);
}
rc = 0;
exit:
str_t_clear(&sign_reversed);
if (hash) {
cp_function_list.CryptDestroyHash(hash);
}
if (hPubKey) {
cp_function_list.CryptDestroyKey(hPubKey);
}
if (hCryptProv) {
cp_function_list.CryptReleaseContext(hCryptProv, 0);
}
if (certificate) {
cp_function_list.CertFreeCertificateContext(certificate);
}
if (hStoreHandle) {
if (!cp_function_list.CertCloseStore(hStoreHandle, CERT_CLOSE_STORE_CHECK_FLAG)) {
LOG_ERROR("CertCloseStore() failed");
}
}
if (rc == 0) {
LOG_TRACE("cryptopro_verify exit");
} else {
LOG_ERROR("cryptopro_verify exit with error. Last error code: 0x%08x",
cp_function_list.GetLastError());
}
return rc;
}

View file

@ -8,22 +8,25 @@
typedef struct cryptopro_context_s {
const char *signer;
const str_t *pin;
const str_t *cert_thumbprint;
const str_t *password;
} cryptopro_context_t;
static inline void
cryptopro_context_set(cryptopro_context_t *ctx, const char *signer, const str_t *pin)
cryptopro_context_set(cryptopro_context_t *ctx, const str_t *cert_thumbprint, const str_t *password)
{
assert(ctx != NULL);
ctx->signer = signer;
ctx->pin = pin;
ctx->cert_thumbprint = cert_thumbprint;
ctx->password = password;
}
bool cryptopro_init(const char* cp_file);
int cryptopro_sign(const cryptopro_context_t *ctx, const str_t *data, /*out*/ str_t *sign);
int cryptopro_verify(const str_t* cert_thumbprint, const str_t* alg, const str_t *data,
const str_t *sign, bool* is_verified, char** verify_error);
#endif // CRYPTOPRO_H_INCLUDED

View file

@ -1,69 +1,9 @@
#include "gconf_file.h"
#include "glib_utils.h"
#include "logger.h"
#include <assert.h>
/** Returns the length of the gchar string @str
*/
static inline size_t gstr_byte_len(const gchar* str)
{
return strlen((char*) str);
}
/*
* Copies the gchar string pointed by src into the content pointed by dst, including
* the terminating null character (and stopping at that point).
* Uses to avoid dependence from glib.
* @src: gchar string to be copied
* @dst: pointer to destination of type str_t where content is to be copied
* Return value: 0 - success, -1 - error
*/
static int
copy_gchar2strt(const gchar* src, str_t* dst)
{
assert(src != NULL);
assert(dst != NULL);
size_t len = gstr_byte_len(src);
char* data = (char*) malloc(len + 1);
if (data == NULL) {
LOG_ERROR("gchar2strt exit with error: could not allocate memory for dst");
return -1;
}
dst->data = data;
dst->len = len;
memcpy(dst->data, src, dst->len + 1);
dst->data[dst->len] = '\0';
return 0;
}
/*
* Copies the gchar string pointed by src into the content pointed by return value,
* including the terminating null character (and stopping at that point).
* Uses to avoid dependence from glib.
* @src: gchar string to be copied
* Return value: pointer to destination where content is to be copied
*/
static char*
copy_gchar2char(const gchar* src)
{
assert(src != NULL);
// char* tmp_end = strchr(src, '\0');
char* dst = NULL;
size_t len = gstr_byte_len(src);
dst = (char*) malloc(len + 1);
if (dst == NULL) {
LOG_ERROR("gchar2char exit with error: could not allocate memory for dst");
return NULL;
}
memcpy(dst, src, len + 1);
return dst;
}
/* Returns the string value from GKeyFile associated with key under section and converted to str_t
* @ini_file: GKeyFile
* @section: section name in config file

49
src/utils/glib_utils.c Normal file
View file

@ -0,0 +1,49 @@
#include "glib_utils.h"
#include "logger.h"
#include <assert.h>
/** Returns the length of the gchar string @str
*/
static inline size_t
gstr_byte_len(const gchar* str)
{
return strlen((char*) str);
}
int
copy_gchar2strt(const gchar* src, str_t* dst)
{
assert(src != NULL);
assert(dst != NULL);
size_t len = gstr_byte_len(src);
char* data = (char*) malloc(len + 1);
if (data == NULL) {
LOG_ERROR("gchar2strt exit with error: could not allocate memory for dst");
return -1;
}
dst->data = data;
dst->len = len;
memcpy(dst->data, src, dst->len + 1);
dst->data[dst->len] = '\0';
return 0;
}
char*
copy_gchar2char(const gchar* src)
{
assert(src != NULL);
char* dst = NULL;
size_t len = gstr_byte_len(src);
dst = (char*) malloc(len + 1);
if (dst == NULL) {
LOG_ERROR("gchar2char exit with error: could not allocate memory for dst");
return NULL;
}
memcpy(dst, src, len + 1);
return dst;
}

32
src/utils/glib_utils.h Normal file
View file

@ -0,0 +1,32 @@
#ifndef GLIB_UTULS_H_INCLUDED
#define GLIB_UTULS_H_INCLUDED
#include "str_t.h"
#include <glib.h>
/*
* Copies the gchar string pointed by src into the content pointed by dst, including
* the terminating null character (and stopping at that point).
* Uses to avoid dependence from glib.
* @src: gchar string to be copied
* @dst: pointer to destination of type str_t where content is to be copied
* Return value: 0 - success, -1 - error
*/
int
copy_gchar2strt(const gchar* src, str_t* dst);
/*
* Copies the gchar string pointed by src into the content pointed by return value,
* including the terminating null character (and stopping at that point).
* Uses to avoid dependence from glib.
* @src: gchar string to be copied
* Return value: pointer to destination where content is to be copied
*/
char*
copy_gchar2char(const gchar* src);
#endif // GLIB_UTULS_H_INCLUDED

239
src/utils/json_parser.c Normal file
View file

@ -0,0 +1,239 @@
#include "json_parser.h"
#include "glib_utils.h"
#include "logger.h"
#include <assert.h>
#include <json-glib/json-glib.h>
typedef struct json_parser_s {
JsonParser *parser;
void *root;
json_parser_type root_type;
} json_parser_t;
static JsonObject* get_object_from_node(JsonNode *node);
static int json_get_object_member_as_string(Hjson_parser hparser, const char* field_name,
/*out*/ str_t* field_value);
Hjson_parser
json_parser_create(const char* data, size_t data_len)
{
GError* gerror = NULL;
JsonNode* json_node = NULL;
JsonNodeType json_node_type;
const char *desc = "";
LOG_TRACE("json_parser_create enter");
assert(data != NULL && data_len != 0 && "json data is empty");
json_parser_t* parser = calloc (sizeof(json_parser_t), 1);
if (parser == NULL) {
desc = "Could not allocate memory for json_parser_t";
goto error;
}
parser->parser = json_parser_new();
if (parser->parser == NULL) {
desc = "Could not create JsonParser";
goto error;
}
LOG_DEBUG("json_parser_create. json data: %.*s", (int)data_len, data);
if (json_parser_load_from_data(parser->parser, data, data_len, &gerror) != TRUE) {
LOG_ERROR("json_parser_create. json_parser_load_from_data failed: %s", gerror->message);
g_error_free(gerror);
goto error;
}
/* get root node */
json_node = json_parser_get_root(parser->parser);
if (json_node == NULL) {
desc = "Could not parse json object: json_parser_get_root failed";
goto error;
}
json_node_type = json_node_get_node_type(json_node);
if (json_node_type != JSON_NODE_OBJECT) {
desc = "Could not parse json object: unexpected root type";
goto error;
}
parser->root_type = JSON_PARSER_TYPE_OBJECT;
/* try to get root as object */
parser->root = get_object_from_node(json_node);
if (parser->root == NULL) {
desc = "Could not parse json object: json_node_get_object failed";
goto error;
}
return (Hjson_parser)parser;
error:
json_parser_free((Hjson_parser)parser);
LOG_ERROR("json_parser_create exit with error. Desc: %s", desc);
return NULL;;
}
void
json_parser_free(Hjson_parser hparser)
{
json_parser_t* parser = (json_parser_t*) hparser;
if (parser == NULL) return;
if (parser->parser != NULL) {
g_object_unref(parser->parser);
}
free(parser);
}
int
json_get_string_member(Hjson_parser hparser, const char* field_name,
/*out*/ str_t* field_value)
{
LOG_TRACE("json_get_string_member enter");
switch (json_get_object_member_as_string(hparser, field_name, field_value)) {
case 0:
LOG_TRACE("json_get_string_member exit successfully. field_value: %.*s",
(int) field_value->len,
field_value->data);
return 0;
case 1:
LOG_ERROR("Could not retrieve '%.*s' from json object. "
"Desc: field does not exist or it has not proper type. "
"Expected: string member",
(int) field_value->len, field_value->data);
break;
default:
LOG_ERROR("json_get_object_member_as_string failed, member: '%.*s'",
(int) field_value->len, field_value->data);
break;
}
LOG_ERROR("json_get_string_member exit with error");
return -1;
}
static JsonObject*
get_object_from_node(JsonNode *node)
{
JsonObject *ret = NULL;
if (node == NULL) {
LOG_WARN("get_object_from_node. node is NULL");
return NULL;
}
if (!JSON_NODE_HOLDS_OBJECT(node)) {
LOG_ERROR("Bad json scheme. Expected node: object");
return NULL;
}
ret = json_node_get_object(node);
assert(ret != NULL && "Could not extract the node of correct type");
return ret;
}
static int
get_string_from_node(JsonNode *node, /*out*/ const gchar **value)
{
assert(value != NULL);
*value = NULL;
if (node == NULL) {
LOG_WARN("get_string_from_node. node is NULL");
return 0;
}
/* если нод содержит нулевое значение, оставляем наш указатель так же нулевым */
if (JSON_NODE_HOLDS_NULL(node)) {
LOG_DEBUG("get_string_from_node. node holds NULL");
return 0;
}
if (!JSON_NODE_HOLDS_VALUE(node) || G_TYPE_STRING != json_node_get_value_type(node)) {
LOG_ERROR("Bad json scheme. Expected node: string value. Actual node type: %s",
json_node_type_name(node));
return -1;
}
*value = json_node_get_string(node);
if (*value == NULL) {
LOG_ERROR("Bad json node. Expected node: string value");
return -1;
}
return 0;
}
static int
json_object_get_string(JsonObject *object, const gchar *key, /*out*/const gchar** value)
{
JsonNode *node = NULL;
LOG_TRACE("json_object_get_string");
assert(object != NULL);
assert(key != NULL);
assert(value != NULL);
*value = NULL;
node = json_object_get_member(object, key);
if (node == NULL) {
LOG_DEBUG("object member by key '%s' is not exists", key);
/* ret = NULL; */
return 0;
}
return get_string_from_node(node, value);
}
static int
json_get_object_member_as_string(Hjson_parser hparser, const char* field_name,
/*out*/ str_t* field_value)
{
json_parser_t* parser = (json_parser_t*) hparser;
const gchar* value = NULL;
assert(parser != NULL);
assert(parser->root_type == JSON_PARSER_TYPE_OBJECT);
assert(parser->root != NULL);
assert(field_name != NULL);
assert(field_value != NULL && str_t_is_null(*field_value));
LOG_DEBUG("json_get_object_member_as_string. field name: %s", field_name);
if (json_object_get_string((JsonObject*) (parser->root), field_name, &value)) {
return -1;
}
if (value == NULL) {
return 1;
}
if (copy_gchar2strt(value, field_value) != 0) {
LOG_ERROR("json_get_object_member_as_string. Could not copy field_value");
return -1;
}
return 0;
}

31
src/utils/json_parser.h Normal file
View file

@ -0,0 +1,31 @@
#ifndef JSON_PARSER_H_INCLUDED
#define JSON_PARSER_H_INCLUDED
#include "str_t.h"
#include <stdbool.h>
#include <stddef.h>
typedef struct json_parser_t *Hjson_parser;
typedef enum json_parser_type_e {
JSON_PARSER_TYPE_UNKNOWN,
JSON_PARSER_TYPE_STRING,
JSON_PARSER_TYPE_OBJECT,
JSON_PARSER_TYPE_ARRAY
} json_parser_type;
Hjson_parser json_parser_create(const char* data, size_t data_len);
void json_parser_free(Hjson_parser hparser);
int json_get_string_member(Hjson_parser hparser, const char* field_name,
/*out*/ str_t* field_value);
#endif // JSON_PARSER_H_INCLUDED

75
src/utils/jwt.c Normal file
View file

@ -0,0 +1,75 @@
#include "jwt.h"
#include "json_parser.h"
#include "logger.h"
#define JWT_ALG_FIELD_NAME "alg"
int
jwt_get_header_payload_and_sign(const str_t *jwt,
/*out*/ str_t *header,
/*out*/ str_t *header_payload,
/*out*/ str_t *sign)
{
LOG_TRACE("jwt_get_header_payload_and_sign enter");
size_t p_start = 0, p_end = 0;
for (size_t i = 0; i < jwt->len; i++) {
if (jwt->data[i] == '.') {
if (p_start == 0) {
p_start = i + 1;
} else {
p_end = i;
break;
}
}
}
if (p_start == 0 || p_end == 0) {
LOG_ERROR("Could not parse jwt. p_start = %zd, p_end = %zd", p_start, p_end);
return -1;
}
header->data = (char*)jwt->data;
header->len = p_start - 1;
header_payload->data = (char*)jwt->data;
header_payload->len = p_end;
sign->data = (char*)jwt->data + p_end + 1;
sign->len = jwt->len - p_end - 1;
LOG_DEBUG("header: %.*s", (int) header->len, header->data);
LOG_DEBUG("header_payload: %.*s", (int) header_payload->len, header_payload->data);
LOG_DEBUG("sign: %.*s", (int) sign->len, sign->data);
LOG_TRACE("jwt_get_header_payload_and_sign exit");
return 0;
}
int
jwt_get_alg_from_header(const str_t *header, /*out*/ str_t *alg)
{
LOG_TRACE("jwt_get_alg_from_header enter");
Hjson_parser parser = json_parser_create(header->data, header->len);
if (parser == NULL) {
goto error;
}
if (json_get_string_member(parser, JWT_ALG_FIELD_NAME, alg)) {
goto error;
}
json_parser_free(parser);
LOG_TRACE("jwt_get_alg_from_header exit");
return 0;
error:
json_parser_free(parser);
LOG_ERROR("jwt_get_alg_from_header exit with error");
return -1;
}

13
src/utils/jwt.h Normal file
View file

@ -0,0 +1,13 @@
#ifndef JWT_H_INCLUDED
#define JWT_H_INCLUDED
#include "str_t.h"
int jwt_get_header_payload_and_sign(const str_t *jwt,
/*out*/ str_t *header,
/*out*/ str_t *header_payload,
/*out*/ str_t *sign);
int jwt_get_alg_from_header(const str_t *header, /*out*/ str_t *alg);
#endif // JWT_H_INCLUDED

View file

@ -49,3 +49,51 @@ str_t_to_string(const str_t* str)
return string;
}
static int
char2int(char input)
{
if (input >= '0' && input <= '9') {
return input - '0';
}
if (input >= 'A' && input <= 'F') {
return input - 'A' + 10;
}
if (input >= 'a' && input <= 'f') {
return input - 'a' + 10;
}
LOG_ERROR("char2int exit with error: invalid argument, '%c'", input);
assert("char2int exit with error: invalid argument");
return -1;
}
int
hex_to_bin(const str_t* hex, /*out*/ str_t* bin)
{
bin->len = hex->len / 2;
bin->data = malloc(bin->len + 1);
if (bin->data == NULL) {
LOG_ERROR("Could not allocate memory for bin value (%zd bytes)", bin->len + 1);
goto error;
}
const char* src = hex->data;
char* dst = bin->data;
while (*src && src[1]) {
*(dst++) = char2int(*src)*16 + char2int(src[1]);
src += 2;
}
return 0;
error:
str_t_clear(bin);
LOG_ERROR("hex_to_bin exit with error");
return -1;
}

View file

@ -92,4 +92,6 @@ int str_t_copy(str_t* dst, const str_t* src);
char* str_t_to_string(const str_t* str);
int hex_to_bin(const str_t* hex, /*out*/ str_t* bin);
#endif // STR_T_H_INCLUDED

30
tests/CMakeLists.txt Normal file
View file

@ -0,0 +1,30 @@
project (test-ervu-sign-module LANGUAGES C)
include_directories (
../src
${GLIB2_INCLUDE_DIRS}
${JSONGLIB_INCLUDE_DIRS}
)
set (ESM_SOURCES
../src/utils/glib_utils.c
../src/utils/json_parser.c
../src/utils/jwt.c
../src/utils/logger.c
)
set (HEADERS
utils/test_jwt.h
)
set (SOURCES
main.c
utils/test_jwt.c
"${ESM_SOURCES}"
)
add_executable(test-ervu-sign-module ${HEADERS} ${SOURCES})
target_link_libraries(test-ervu-sign-module cunit glib-2.0 json-glib-1.0 gobject-2.0)
add_test(NAME test-ervu-sign-module COMMAND test-ervu-sign-module)

30
tests/main.c Normal file
View file

@ -0,0 +1,30 @@
#include "utils/test_jwt.h"
#include <CUnit/Basic.h>
int main(int arv, char** argc)
{
int failures = -1;
(void)arv;
(void)argc;
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();
CU_pSuite suite_jwt = CU_add_suite("JWTSuit", NULL, NULL);
if (NULL == suite_jwt) {
goto exit;
}
if (NULL == CU_ADD_TEST(suite_jwt, test_jwt_get_header_payload_and_sign)) goto exit;
if (NULL == CU_ADD_TEST(suite_jwt, test_jwt_get_alg_from_header)) goto exit;
CU_basic_set_mode(CU_BRM_NORMAL);
CU_basic_run_tests();
exit:
failures = CU_get_number_of_failures();
CU_cleanup_registry();
return (failures == 0) ? 0 : -1;
}

68
tests/utils/test_jwt.c Normal file
View file

@ -0,0 +1,68 @@
#include "test_jwt.h"
#include "utils/jwt.h"
#include <CUnit/Basic.h>
#define JWT "eyJ2ZXIiOjEsInR5cCI6IkpXVCIsInNidCI6ImF1dGhvcml6YXRpb25fY29kZSIsImFsZyI6IkdP" \
"U1QzNDEwXzIwMTJfMjU2In0.eyJuYmYiOjE2ODE3MzI4ODQsInNjb3BlIjoib3BlbmlkIiwiYXV0" \
"aF90aW1lIjoxNjgxNzMyODc2LCJpc3MiOiJodHRwOlwvXC9lc2lhLXBvcnRhbDEudGVzdC5nb3N1" \
"c2x1Z2kucnVcLyIsInVybjplc2lhOnNpZCI6Ijg3NWQzNDZhLTgzMTktMmM1MS05ZDYwLTYwZmRk" \
"MzE4MGVhOSIsInVybjplc2lhOmNsaWVudDpzdGF0ZSI6Ijg5MzgzNDdhLTI0MmMtNGVhYy05NTdk" \
"LTk4MDgzNTAzYzUwNyIsImF1dGhfbXRoZCI6IlBXRCIsInVybjplc2lhOnNiaiI6eyJ1cm46ZXNp" \
"YTpzYmo6bHZsIjoiUCIsInVybjplc2lhOnNiajp0eXAiOiJQIiwidXJuOmVzaWE6c2JqOm9pZCI6" \
"MTAwMDQwMDgyMywidXJuOmVzaWE6c2JqOm5hbSI6Ik9JRC4xMDAwNDAwODIzIn0sImV4cCI6MTY4" \
"MTczMzEyNCwicGFyYW1zIjp7fSwiaWF0IjoxNjgxNzMyODg0LCJjbGllbnRfaWQiOiI1MDAyMDEi" \
"fQ.8EBVu5Cizok66n1sIrVmS07N_4j9TuaKHlQffr98hwv9amUN5zpckH4dyJtO6IA-8UkhZ19ww" \
"Of0F_VCmhOqmQ"
#define JWT_HEADER "eyJ2ZXIiOjEsInR5cCI6IkpXVCIsInNidCI6ImF1dGhvcml6YXRpb25fY29kZSIsImFsZyI6IkdP" \
"U1QzNDEwXzIwMTJfMjU2In0"
#define JWT_HEADER_PAYLOAD "eyJ2ZXIiOjEsInR5cCI6IkpXVCIsInNidCI6ImF1dGhvcml6YXRpb25fY29kZSIsImFsZ" \
"yI6IkdPU1QzNDEwXzIwMTJfMjU2In0.eyJuYmYiOjE2ODE3MzI4ODQsInNjb3BlIjoib3" \
"BlbmlkIiwiYXV0aF90aW1lIjoxNjgxNzMyODc2LCJpc3MiOiJodHRwOlwvXC9lc2lhLXB" \
"vcnRhbDEudGVzdC5nb3N1c2x1Z2kucnVcLyIsInVybjplc2lhOnNpZCI6Ijg3NWQzNDZh" \
"LTgzMTktMmM1MS05ZDYwLTYwZmRkMzE4MGVhOSIsInVybjplc2lhOmNsaWVudDpzdGF0Z" \
"SI6Ijg5MzgzNDdhLTI0MmMtNGVhYy05NTdkLTk4MDgzNTAzYzUwNyIsImF1dGhfbXRoZC" \
"I6IlBXRCIsInVybjplc2lhOnNiaiI6eyJ1cm46ZXNpYTpzYmo6bHZsIjoiUCIsInVybjp" \
"lc2lhOnNiajp0eXAiOiJQIiwidXJuOmVzaWE6c2JqOm9pZCI6MTAwMDQwMDgyMywidXJu" \
"OmVzaWE6c2JqOm5hbSI6Ik9JRC4xMDAwNDAwODIzIn0sImV4cCI6MTY4MTczMzEyNCwic" \
"GFyYW1zIjp7fSwiaWF0IjoxNjgxNzMyODg0LCJjbGllbnRfaWQiOiI1MDAyMDEifQ"
#define JWT_SIGN "8EBVu5Cizok66n1sIrVmS07N_4j9TuaKHlQffr98hwv9amUN5zpckH4dyJtO6IA-8UkhZ19ww" \
"Of0F_VCmhOqmQ"
#define JWT_HEADER_DECODED "{\"ver\":1,\"typ\":\"JWT\",\"sbt\":\"authorization_code\"," \
"\"alg\":\"GOST3410_2012_256\"}"
#define JWT_ALG "GOST3410_2012_256"
void test_jwt_get_header_payload_and_sign()
{
int rc;
str_t jwt = str_t_const(JWT);
str_t sign = str_t_null;
str_t header = str_t_null;
str_t header_payload = str_t_null;
rc = jwt_get_header_payload_and_sign(&jwt, &header, &header_payload, &sign);
CU_ASSERT_EQUAL(rc, 0);
CU_ASSERT_NSTRING_EQUAL(header.data, JWT_HEADER, header.len);
CU_ASSERT_NSTRING_EQUAL(header_payload.data, JWT_HEADER_PAYLOAD, header_payload.len);
CU_ASSERT_NSTRING_EQUAL(sign.data, JWT_SIGN, sign.len);
}
void test_jwt_get_alg_from_header()
{
int rc;
str_t header = str_t_const(JWT_HEADER_DECODED);
str_t alg = str_t_null;
rc = jwt_get_alg_from_header(&header, &alg);
CU_ASSERT_EQUAL(rc, 0);
CU_ASSERT_NSTRING_EQUAL(alg.data, JWT_ALG, alg.len);
str_t_clear(&alg);
}

8
tests/utils/test_jwt.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef TEST_JWT_H_INCLUDED
#define TEST_JWT_H_INCLUDED
void test_jwt_get_header_payload_and_sign();
void test_jwt_get_alg_from_header();
#endif // TEST_JWT_H_INCLUDED

View file

@ -0,0 +1,111 @@
В версии 1.1.0 появились следующие функции:
- Проверка подписи маркера доступа
- Вывод номера версии приложения
Для обновления приложения необходимо выполнить следующие шаги (команды выполняются от имени root):
1. Остановить службу:
```
systemctl stop ervu-sign-module
```
2. Обновить приложение:
```
cp ./ervu-sign-module /opt/ervu-sign-module/
```
3. Обновить конфигурационный файл /etc/ervu-sign-module.conf. Для этого:
3.1. Перенести настройку cp_file из секции [sign] в секцию [main]
3.2. Задать SHA1 отпечаток сертификата, которым ИС подписывает секрет, для этого:
3.2.1. В конфигурационном файле получить значение <signer_subject>
3.2.2. Получить перечень свойств сертификата (команда выполняется от имени ervu):
```
/opt/cprocsp/bin/amd64/certmgr -list -dn <signer_subject>
```
Из полученного перечня свойств сертификата необходимо получить поле "SHA1 отпечаток".
3.2.3. В конфигурационном файле в секции [sign] задать полученное значение полю sign_cert_thumbprint:
[sign]
sign_cert_thumbprint = <значение поля "SHA1 отпечаток", полученное на предыдущем шаге>
3.2.4. Настройку signer_subject нужно удалить.
3.3. Переименовать настройку с паролем от контейнера (было - pin, стало - sign_cert_password)
[sign]
sign_cert_password = <пароль, который раньше задавался в поле pin>
4. Установить сертификат ЕСИА. Для этого:
4.1. Скачать сертификаты тестовой и продуктивной сред ЕСИА, используемые для формирования электронных подписей ответов как поставщика, по ссылке: https://esia.gosuslugi.ru/public/esia.zip
4.2. Из списка сертификатов выбрать тот, который подходит под конфигурацию стенда (в зависимости от алгоритма подписи и от того, является ли среда тестовой или продуктивной):
| Среда \ Алгоритм подписания | RS256 | GOST3410_2012_256 |
|-----------------------------|-----------------------------------------------------|-----------------------------------------------------|
| | RSA_TESIA_2024.cer | TESIA GOST 2012 new.cer |
| Тестовая | Отпечаток: 6a3813240f6cc285e6d4e6d9a4bfeb26038195d8 | Отпечаток: e654f6114dc19bae84c5748794a4fd7797726e71 |
| | Срок действия: по 06.08.2034 | Срок действия: по 24.04.2025 |
|-----------------------------|-----------------------------------------------------|-----------------------------------------------------|
| | RSA_PROD_2024.cer | ГОСТ+ПРОД+24-25.cer |
| Продуктивная | Отпечаток: 29c2f9c8e064d73509eadd685a5508712735e303 | Отпечаток: 20ad21785b9161ef46f075d4dc23c34e1e349228 |
| | Срок действия: по 12.05.2025 | Срок действия: по 08.06.2025 |
-------------------------------------------------------------------------------------------------------------------------------------------
4.3. Установить выбранный сертификат (команда выполняется от имени ervu):
```
/opt/cprocsp/bin/amd64/certmgr -install -file '<esia_cert>.cer'
```
Результатом команды будет список полей сертификата. Из него необходимо получить следующие значения:
- Поле "SHA1 отпечаток": это значение нужно будет указать в конфигурационном файле в качестве настройки <esia_cert_thumbprint>.
- Поле "Алгоритм подписи": проверить, что значение совпадает с настройками ИС в ЕСИА.
- Поле "Истекает": проверить, что срок действия сертификата не истёк.
4.4. В конфигурационном файле добавить секцию [verify]:
[verify]
#location = /verify
esia_cert_thumbprint =
4.5. Прописать SHA1 отпечаток сертификата ЕСИА:
[verify]
esia_cert_thumbprint = <значение, полученное на шаге 4.3.>
Примечание. Пример конфигурационного файла можно посмотреть в conf/ervu-sign-module.conf.example
5. Запустить службу:
```
systemctl start ervu-sign-module
```
6. Обновить конфигурационный файл /etc/nginx/nginx.conf:
заменить строку:
```
location = /sign {
```
на:
```
location / {
```
Примечание. Пример конфигурационного файла можно посмотреть в conf/nginx.conf
7. Перезапустить nginx:
```
systemctl reload nginx
```
8. Проверить версию приложения:
```
curl -v http://127.0.0.1/version
```
Статус-код ответа должен быть равен 200 OK.
В ответе должна быть возвращена строчка "1.1.0".

View file

@ -18,7 +18,7 @@ cd -
1. Установить зависимости для сборки приложения:
``` bash
apt-get install cmake gcc gcc10
apt-get install glib2-devel libfcgi-devel
apt-get install glib2-devel libfcgi-devel libjson-glib libjson-glib-devel
```
2. Собрать приложение из исходников:

View file

@ -11,11 +11,21 @@ adduser --system --no-create-home --user-group ervu
1. Скачать КриптоПро 5 R3 КС3.
2. Установить КриптоПро. Для этого выполнить команды от имени root:
2. Установить КриптоПро.
2.1. Для продуктивного контура:
2.1.1. Установить КриптоПро c конфигурацией kc2. Для этого выполнить команды от имени root:
``` bash
tar -zxvf linux-amd64.tgz && cd ./linux-amd64/
./install.sh
apt-get install ./cprocsp-pki-cades-64-2.0.14815-1.amd64.rpm
./install.sh kc2 cprocsp-pki-cades
cd -
```
2.1.2. Установить ПАК "Соболь" по инструкции: https://support.cryptopro.ru/index.php?/Knowledgebase/Article/View/285/2/ispolzovnie-pak-sobol-v-kchestve-dsch-v-kriptopro-csp-n-linux
2.2. Для тестового контура:
2.2.1. Установить КриптоПро c конфигурацией kc1. Для этого выполнить команды от имени root:
``` bash
tar -zxvf linux-amd64.tgz && cd ./linux-amd64/
./install.sh kc1 cprocsp-pki-cades
cd -
```
@ -24,9 +34,11 @@ cd -
/opt/cprocsp/sbin/amd64/cpconfig -license -set "Номер"
```
4. Запросить у руководителя проекта реестра повесток контейнер с ключом ИС, зарегистрированной в ЕСИА.
### Установка ключевой пары ИС, зарегистрированной в ЕСИА
5. Установить полученную ключевую пару для пользователя, от которого запускается модуль подписания (команды выполняются от имени **ervu**).
1. Запросить у руководителя проекта реестра повесток контейнер с ключом ИС, зарегистрированной в ЕСИА.
2. Установить полученную ключевую пару для пользователя, от которого запускается модуль подписания (команды выполняются от имени **ervu**).
Внимание! В следующих командах необходимо заменить ***key_cont.000*** на название контейнера, полученного на предыдущем шаге.
``` bash
@ -38,22 +50,48 @@ chown -R ervu:ervu /var/opt/cprocsp/keys/ervu/key_cont.000/
/opt/cprocsp/bin/amd64/csptest -absorb -certs -autoprov
```
6. Получить перечень сертификатов:
3. Получить перечень сертификатов:
``` bash
sudo -u ervu /opt/cprocsp/bin/amd64/certmgr -list
/opt/cprocsp/bin/amd64/certmgr -list
```
Получить следующие значения:
- Поле "Субъект": любая подстрока из этого поля является signer_subject (нужно будет указать в конфигурационном файле модуля подписания)
- Поле "Ссылка на ключ": убедиться, что значение равно "Есть" (иначе проверьте выполнение предыдущих шагов)
- Поле "SHA1 отпечаток": это значение нужно будет указать в конфигурационном файле модуля подписания в качестве настройки \<sign_cert_thumbprint\>
- Поле "Ссылка на ключ": убедиться, что значение равно "Есть" (иначе необходимо проверить выполнение предыдущих шагов)
7. Сделать тестовую подпись с помощью установленного контейнера и полученных значений \<signer_subject\> и \<pin\>:
4. (опционально) Сделать тестовую подпись с помощью установленного контейнера и полученных значений \<sign_cert_thumbprint\> и \<sign_cert_password\>:
``` bash
touch test.txt
sudo -u ervu /opt/cprocsp/bin/amd64/cryptcp -signf -dn <signer_subject> ./test.txt
/opt/cprocsp/bin/amd64/cryptcp -signf -thumbprint <sign_cert_thumbprint> ./test.txt
rm -f test.txt test.txt.sgn
```
### Установка сертификата ЕСИА
1. Скачать сертификаты тестовой и продуктивной сред ЕСИА, используемые для формирования электронных подписей ответов как поставщика, по ссылке: https://esia.gosuslugi.ru/public/esia.zip
2. Из списка сертификатов выбрать тот, который подходит под конфигурацию стенда (в зависимости от алгоритма подписи и от того, является ли среда тестовой или продуктивной):
| Среда \ Алгоритм подписания | RS256 | GOST3410_2012_256 |
|-----------------------------|-----------------------------------------------------|-----------------------------------------------------|
| | RSA_TESIA_2024.cer | TESIA GOST 2012 new.cer |
| Тестовая | Отпечаток: 6a3813240f6cc285e6d4e6d9a4bfeb26038195d8 | Отпечаток: e654f6114dc19bae84c5748794a4fd7797726e71 |
| | Срок действия: по 06.08.2034 | Срок действия: по 24.04.2025 |
|-----------------------------|-----------------------------------------------------|-----------------------------------------------------|
| | RSA_PROD_2024.cer | ГОСТ+ПРОД+24-25.cer |
| Продуктивная | Отпечаток: 29c2f9c8e064d73509eadd685a5508712735e303 | Отпечаток: 20ad21785b9161ef46f075d4dc23c34e1e349228 |
| | Срок действия: по 12.05.2025 | Срок действия: по 08.06.2025 |
3. Установить выбранный сертификат для пользователя, от которого запускается модуль подписания (команды выполняются от имени **ervu**).
``` bash
/opt/cprocsp/bin/amd64/certmgr -install -file '<esia_cert>.cer'
```
Результатом команды будет список полей сертификата. Из него необходимо получить следующие значения:
- Поле "SHA1 отпечаток": это значение нужно будет указать в конфигурационном файле модуля подписания в качестве настройки \<esia_cert_thumbprint\>.
- Поле "Алгоритм подписи": проверить, что значение совпадает с настройками ИС в ЕСИА.
- Поле "Истекает": проверить, что срок действия сертификата не истёк.
### Установка и настройка nginx
Дальнейшие шаги выполняются от имени root:
@ -77,7 +115,7 @@ systemctl start nginx
1. Установить зависимости для запуска приложения:
``` bash
apt-get install glib2 libfcgi
apt-get install glib2 libfcgi libjson-glib
```
2. Скопировать исполняемый файл:
@ -133,4 +171,21 @@ curl -v http://127.0.0.1/sign -H "Content-Type: text/plain" -d "test"
<
REFlyzGQrCjX9DvA7hWwN9vf5kPqBxcG4TLYnXUHnAS9_G-sLAFvaJei2OhxpaWNraHbOv_mMsM_bcDsXWiC0Q
* Connection #0 to host 127.0.0.1 left intact
```
```
### Версия приложения
Для того, чтобы узнать версию приложения, введите команду:
``` bash
curl -v http://127.0.0.1/version
```
В ответе будет получена версия приложения (статус-код ответа должен быть равен 200 OK).
### Примечания
Подробнее о работе с КриптоПро CSP здесь:
- https://www.altlinux.org/%D0%9A%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D0%9F%D1%80%D0%BE
- https://wiki.astralinux.ru/pages/viewpage.action?pageId=32833902&ysclid=m2si404crb648040570