diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ce7aee..7b6c24f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/Dockerfile.micord b/Dockerfile.micord index de8d1a0..06ebcb9 100644 --- a/Dockerfile.micord +++ b/Dockerfile.micord @@ -1,4 +1,4 @@ -ARG BUILDER_IMAGE=repo.micord.ru/alt/alt-cprocsp:c10f1-5.0.13000-20240827 +ARG BUILDER_IMAGE=repo.micord.ru/alt/alt-cprocsp:c10f1-5.0.13000-20241129 ARG RUNTIME_IMAGE=registry.altlinux.org/basealt/altsp:c10f1 FROM ${BUILDER_IMAGE} AS builder @@ -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"] diff --git a/README.md b/README.md index 096a6b7..1855d16 100644 --- a/README.md +++ b/README.md @@ -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 отпечаток сертификата ЕСИА* diff --git a/conf/TESIA GOST 2012 new.cer b/conf/TESIA GOST 2012 new.cer new file mode 100644 index 0000000..69a3d5c Binary files /dev/null and b/conf/TESIA GOST 2012 new.cer differ diff --git a/conf/b0fd8eb959d9489d5b7b4c143a06cad7952a0744.crl b/conf/b0fd8eb959d9489d5b7b4c143a06cad7952a0744.crl new file mode 100644 index 0000000..51ebdaa Binary files /dev/null and b/conf/b0fd8eb959d9489d5b7b4c143a06cad7952a0744.crl differ diff --git a/conf/ervu-sign-module.conf.example b/conf/ervu-sign-module.conf.example index 76dfb54..5d32295 100644 --- a/conf/ervu-sign-module.conf.example +++ b/conf/ervu-sign-module.conf.example @@ -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 = diff --git a/conf/nginx-docker.conf b/conf/nginx-docker.conf index 362e9b9..7916831 100644 --- a/conf/nginx-docker.conf +++ b/conf/nginx-docker.conf @@ -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; } } diff --git a/conf/nginx.conf b/conf/nginx.conf index 3703968..3498317 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -35,7 +35,7 @@ http { listen 80; server_name localhost; - location = /sign { + location / { fastcgi_pass localhost:9009; include fastcgi_params; } diff --git a/conf/test_ca_rtk3.cer b/conf/test_ca_rtk3.cer new file mode 100644 index 0000000..394759e --- /dev/null +++ b/conf/test_ca_rtk3.cer @@ -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----- diff --git a/src/fcgisrv/fcgi_map.c b/src/fcgisrv/fcgi_map.c index fef04c5..827967c 100644 --- a/src/fcgisrv/fcgi_map.c +++ b/src/fcgisrv/fcgi_map.c @@ -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]); + } +} \ No newline at end of file diff --git a/src/fcgisrv/fcgi_server.c b/src/fcgisrv/fcgi_server.c index f5a15d1..80a6260 100644 --- a/src/fcgisrv/fcgi_server.c +++ b/src/fcgisrv/fcgi_server.c @@ -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"); diff --git a/src/fcgisrv/fcgi_server_internal.h b/src/fcgisrv/fcgi_server_internal.h index 227f110..930b46b 100644 --- a/src/fcgisrv/fcgi_server_internal.h +++ b/src/fcgisrv/fcgi_server_internal.h @@ -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 diff --git a/src/fcgisrv/fcgi_utils.c b/src/fcgisrv/fcgi_utils.c index ae25c28..9066d7f 100644 --- a/src/fcgisrv/fcgi_utils.c +++ b/src/fcgisrv/fcgi_utils.c @@ -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) { diff --git a/src/fcgisrv/fcgi_utils.h b/src/fcgisrv/fcgi_utils.h index 2202237..bd3197c 100644 --- a/src/fcgisrv/fcgi_utils.h +++ b/src/fcgisrv/fcgi_utils.h @@ -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))) { diff --git a/src/main_conf.c b/src/main_conf.c index 6bfcd56..feeb0c4 100644 --- a/src/main_conf.c +++ b/src/main_conf.c @@ -3,8 +3,9 @@ #include #include -#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)); } diff --git a/src/main_conf.h b/src/main_conf.h index 2e32634..c0af5d7 100644 --- a/src/main_conf.h +++ b/src/main_conf.h @@ -5,6 +5,7 @@ typedef struct main_conf_s { int worker_processes; + char *cp_file; /* файл криптопровайдера */ char *conf_file; } main_conf_t; diff --git a/src/modules/service_sign.c b/src/modules/service_sign.c index e368834..af94665 100644 --- a/src/modules/service_sign.c +++ b/src/modules/service_sign.c @@ -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) { diff --git a/src/modules/service_sign.h b/src/modules/service_sign.h index 9f999d0..3bbd02c 100644 --- a/src/modules/service_sign.h +++ b/src/modules/service_sign.h @@ -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; diff --git a/src/modules/service_verify.c b/src/modules/service_verify.c new file mode 100644 index 0000000..fd82789 --- /dev/null +++ b/src/modules/service_verify.c @@ -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; +} diff --git a/src/modules/service_verify.h b/src/modules/service_verify.h new file mode 100644 index 0000000..fdf7153 --- /dev/null +++ b/src/modules/service_verify.h @@ -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 \ No newline at end of file diff --git a/src/modules/service_version.c b/src/modules/service_version.c new file mode 100644 index 0000000..c6313db --- /dev/null +++ b/src/modules/service_version.c @@ -0,0 +1,58 @@ +#include "service_version.h" + +#include "version.h" + +#include "fcgisrv/fcgi_utils.h" + +#include "utils/logger.h" + +#include + + +#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; +} diff --git a/src/modules/service_version.h b/src/modules/service_version.h new file mode 100644 index 0000000..18e59f0 --- /dev/null +++ b/src/modules/service_version.h @@ -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 diff --git a/src/service_manager.c b/src/service_manager.c index 5de04de..03092e6 100644 --- a/src/service_manager.c +++ b/src/service_manager.c @@ -1,5 +1,7 @@ #include "service_manager.h" +#include "utils/cryptopro.h" + #include @@ -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) { diff --git a/src/service_manager.h b/src/service_manager.h index b56c9a2..7c30f88 100644 --- a/src/service_manager.h +++ b/src/service_manager.h @@ -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; diff --git a/src/utils/base64.c b/src/utils/base64.c index cd4de4f..4c9381e 100644 --- a/src/utils/base64.c +++ b/src/utils/base64.c @@ -1,5 +1,7 @@ #include "base64.h" +#include "logger.h" + #include #include @@ -79,4 +81,89 @@ encode_base64_url(str_t *dst, const str_t *src) dst->data[n * 4 + 3] = basis64_url[d]; } } -} \ No newline at end of file +} + +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; +} diff --git a/src/utils/base64.h b/src/utils/base64.h index 55b199d..6ec70e4 100644 --- a/src/utils/base64.h +++ b/src/utils/base64.h @@ -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 \ No newline at end of file diff --git a/src/utils/capi.c b/src/utils/capi.c index 7ecf64b..8cba246 100644 --- a/src/utils/capi.c +++ b/src/utils/capi.c @@ -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; diff --git a/src/utils/capi.h b/src/utils/capi.h index 9277014..555ffb9 100644 --- a/src/utils/capi.h +++ b/src/utils/capi.h @@ -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; diff --git a/src/utils/cryptopro.c b/src/utils/cryptopro.c index a3b54d3..b93b91f 100644 --- a/src/utils/cryptopro.c +++ b/src/utils/cryptopro.c @@ -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; -} \ No newline at end of file +} + +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; +} diff --git a/src/utils/cryptopro.h b/src/utils/cryptopro.h index a580216..cd3e1d8 100644 --- a/src/utils/cryptopro.h +++ b/src/utils/cryptopro.h @@ -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 diff --git a/src/utils/gconf_file.c b/src/utils/gconf_file.c index ecd57f7..4bcfc11 100644 --- a/src/utils/gconf_file.c +++ b/src/utils/gconf_file.c @@ -1,69 +1,9 @@ #include "gconf_file.h" +#include "glib_utils.h" #include "logger.h" #include -/** 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 diff --git a/src/utils/glib_utils.c b/src/utils/glib_utils.c new file mode 100644 index 0000000..b08797b --- /dev/null +++ b/src/utils/glib_utils.c @@ -0,0 +1,49 @@ +#include "glib_utils.h" + +#include "logger.h" + +#include + +/** 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; +} \ No newline at end of file diff --git a/src/utils/glib_utils.h b/src/utils/glib_utils.h new file mode 100644 index 0000000..3873e1d --- /dev/null +++ b/src/utils/glib_utils.h @@ -0,0 +1,32 @@ +#ifndef GLIB_UTULS_H_INCLUDED +#define GLIB_UTULS_H_INCLUDED + +#include "str_t.h" + +#include + + +/* + * 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 diff --git a/src/utils/json_parser.c b/src/utils/json_parser.c new file mode 100644 index 0000000..8f2b797 --- /dev/null +++ b/src/utils/json_parser.c @@ -0,0 +1,239 @@ +#include "json_parser.h" + +#include "glib_utils.h" +#include "logger.h" + +#include +#include + + +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; +} diff --git a/src/utils/json_parser.h b/src/utils/json_parser.h new file mode 100644 index 0000000..a99ade1 --- /dev/null +++ b/src/utils/json_parser.h @@ -0,0 +1,31 @@ +#ifndef JSON_PARSER_H_INCLUDED +#define JSON_PARSER_H_INCLUDED + +#include "str_t.h" + +#include +#include + + +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 diff --git a/src/utils/jwt.c b/src/utils/jwt.c new file mode 100644 index 0000000..91ae4b7 --- /dev/null +++ b/src/utils/jwt.c @@ -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; +} diff --git a/src/utils/jwt.h b/src/utils/jwt.h new file mode 100644 index 0000000..f268849 --- /dev/null +++ b/src/utils/jwt.h @@ -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 \ No newline at end of file diff --git a/src/utils/str_t.c b/src/utils/str_t.c index ac47a7c..5c3895b 100644 --- a/src/utils/str_t.c +++ b/src/utils/str_t.c @@ -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; +} \ No newline at end of file diff --git a/src/utils/str_t.h b/src/utils/str_t.h index 6b28404..3e1707f 100644 --- a/src/utils/str_t.h +++ b/src/utils/str_t.h @@ -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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..6351e3f --- /dev/null +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/main.c b/tests/main.c new file mode 100644 index 0000000..b17b044 --- /dev/null +++ b/tests/main.c @@ -0,0 +1,30 @@ +#include "utils/test_jwt.h" + +#include + +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; +} diff --git a/tests/utils/test_jwt.c b/tests/utils/test_jwt.c new file mode 100644 index 0000000..eb27dd9 --- /dev/null +++ b/tests/utils/test_jwt.c @@ -0,0 +1,68 @@ +#include "test_jwt.h" +#include "utils/jwt.h" + +#include + +#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); +} \ No newline at end of file diff --git a/tests/utils/test_jwt.h b/tests/utils/test_jwt.h new file mode 100644 index 0000000..979e370 --- /dev/null +++ b/tests/utils/test_jwt.h @@ -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 \ No newline at end of file diff --git a/Инструкция по обновлению с версии 1.0.0 до 1.1.0.txt b/Инструкция по обновлению с версии 1.0.0 до 1.1.0.txt new file mode 100644 index 0000000..a842a54 --- /dev/null +++ b/Инструкция по обновлению с версии 1.0.0 до 1.1.0.txt @@ -0,0 +1,116 @@ +В версии 1.1.0 появились следующие функции: +- Проверка подписи маркера доступа +- Вывод номера версии приложения + +Для обновления приложения необходимо выполнить следующие шаги (команды выполняются от имени root): + +0. Установить новые зависимости: +``` +apt-get install libjson-glib +``` + +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. В конфигурационном файле получить значение + +3.2.2. Получить перечень свойств сертификата (команда выполняется от имени ervu): +``` +/opt/cprocsp/bin/amd64/certmgr -list -dn +``` + +Из полученного перечня свойств сертификата необходимо получить поле "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 '.cer' +``` + +Результатом команды будет список полей сертификата. Из него необходимо получить следующие значения: +- Поле "SHA1 отпечаток": это значение нужно будет указать в конфигурационном файле в качестве настройки . +- Поле "Алгоритм подписи": проверить, что значение совпадает с настройками ИС в ЕСИА. +- Поле "Истекает": проверить, что срок действия сертификата не истёк. + +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". diff --git a/Инструкция по сборке.md b/Инструкция по сборке.md index aef7319..5b320af 100644 --- a/Инструкция по сборке.md +++ b/Инструкция по сборке.md @@ -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. Собрать приложение из исходников: diff --git a/Инструкция по установке.md b/Инструкция по установке.md index ffeb33e..2f1391d 100644 --- a/Инструкция по установке.md +++ b/Инструкция по установке.md @@ -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 отпечаток": это значение нужно будет указать в конфигурационном файле модуля подписания в качестве настройки \ +- Поле "Ссылка на ключ": убедиться, что значение равно "Есть" (иначе необходимо проверить выполнение предыдущих шагов) -7. Сделать тестовую подпись с помощью установленного контейнера и полученных значений \ и \: +4. (опционально) Сделать тестовую подпись с помощью установленного контейнера и полученных значений \ и \: ``` bash touch test.txt -sudo -u ervu /opt/cprocsp/bin/amd64/cryptcp -signf -dn ./test.txt +/opt/cprocsp/bin/amd64/cryptcp -signf -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 '.cer' +``` + +Результатом команды будет список полей сертификата. Из него необходимо получить следующие значения: +- Поле "SHA1 отпечаток": это значение нужно будет указать в конфигурационном файле модуля подписания в качестве настройки \. +- Поле "Алгоритм подписи": проверить, что значение совпадает с настройками ИС в ЕСИА. +- Поле "Истекает": проверить, что срок действия сертификата не истёк. + ### Установка и настройка 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 -``` \ No newline at end of file +``` + +### Версия приложения + +Для того, чтобы узнать версию приложения, введите команду: + +``` 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