
Всем привет! Я Максим, бэкенд-разработчик команды MSB (корпоративная сервисная шина), занимаюсь интеграциями систем для внутренних нужд компании Tele2, и в этом посте хочу поделиться опытом интеграции с “КриптоПро DSS” поверх ГОСТ TLS.
Введение
В связи с экономией на бумаге ростом цифровизации бизнес-процессов, в частности, с постепенным уходом от традиционных бумажных документов к электронному документообороту, возникла потребность реализовать электронную подпись документов у нас в компании.
В качестве сервера электронной подписи используется комплекс "КриптоПро DSS", имеющий возможность встроить двухфакторное подтверждение операций подписания в мобильное приложение, посредством своего DSS SDK.
Мы встроили данный SDK в наше корпоративное мобильное приложение, о котором писала моя коллега в своей статье на Хабре.
Но мой рассказ связан с опытом решения задачи со стороны бэкенда, и мы рассмотрим эту задачу подробнее.
Описание проблемы и её решение
В нашей схеме подключение к серверу электронной подписи осуществляется по ГОСТ TLS с аутентификацией клиента по сертификату.
Но не секрет, что стандартные платформы, а именно горячо любимый мной .NET, не поддерживают российские криптошифры по ГОСТу.
В качестве эксперимента пробовал подключить rtengine, но он не завёлся, и, помимо прочего, он не является сертифицированным средством защиты информации. В таких случаях “КриптоПро” советует использовать "КриптоПро Stunnel".
Изначально, stunnel – это open-source приложение, выступающее в роли шлюза, который принимает незашифрованный трафик и пересылает его на целевой сервер поверх TLS. Часто используется, когда клиент сам не поддерживает TLS-шифрование.
А Stunnel от “КриптоПро” – это практически тот же stunnel, но с поддержкой ГОСТ TLS, а значит, он замечательно подходит для решения нашей проблемы.

Представленная выше схема рабочая, если бы не одно но: согласно политикам безопасности в компании, все запросы во внешнюю сеть Интернет могут осуществляться только через корпоративный прокси. Ванильный stunnel из коробки умеет делать запросы через прокси, но “КриптоПро” эту фичу выпилил в своей редакции.

Чтобы обойти это ограничение, в схему было решено добавить еще одно известное Linux-администраторам приложение socat (еще один шлюз, в своем роде), который умеет делать подключения через HTTP-прокси. Важное условие – HTTP-прокси должен разрешать подключения через метод CONNECT.
В итоге схема станет такой:

Docker
Для упрощения было решено пренебречь правилом “один контейнер – один процесс” и запускать “КриптоПро Stunnel” и socat в одном контейнере. Данный контейнер поднимается в виде sidecar рядом с основным контейнером микросервиса. Это позволяет нашему микросервису общаться с “КриптоПро DSS” так, как будто бы они общались по http-протоколу, а вопросы шифрования трафика по ГОСТ TLS отдаются на откуп контейнеру с stunnel и socat.
Чтобы подготовить образ контейнера, нужно скачать deb-пакет с “КриптоПро CSP” (именно в составе этого дистрибутива и состоит “КриптоПро Stunnel”). К сожалению, скачать пакет нельзя по прямой ссылке, которую можно было бы прописать в Dockerfile (иначе бы статья получилась в два раза короче). Для скачивания нужно пройти регистрацию на сайте “КриптоПро”, и только потом будет дана возможность скачать пакет.
Ниже приведен пример Dockerfile, скриптов инициализации и конфига для “КриптоПро Stunnel”.
Рабочий пример можно также посмотреть здесь.
Dockerfile
FROM debian:buster-slim EXPOSE 80/tcp ARG TZ=Europe/Moscow ENV PATH="/opt/cprocsp/bin/amd64:/opt/cprocsp/sbin/amd64:${PATH}" # stunnel settings ENV STUNNEL_HOST="example.cryptopro.ru:4430" ENV STUNNEL_HTTP_PROXY= ENV STUNNEL_HTTP_PROXY_PORT=80 ENV STUNNEL_HTTP_PROXY_CREDENTIALS= ENV STUNNEL_DEBUG_LEVEL=5 ENV STUNNEL_CERTIFICATE_PFX_FILE=/etc/stunnel/certificate.pfx ENV STUNNEL_CERTIFICATE_PIN_CODE= # dependencies RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ && echo $TZ > /etc/timezone \ && apt-get update \ && apt-get -y install lsb-base curl socat \ && rm -rf /var/lib/apt/lists/* # install cryptopro csp WORKDIR /dist COPY dist/csp_deb.tgz csp_deb.tgz RUN tar -zxvf csp_deb.tgz --strip-components=1 \ && ./install.sh cprocsp-stunnel WORKDIR / COPY conf/ /etc/stunnel COPY bin/docker-entrypoint.sh docker-entrypoint.sh COPY bin/stunnel-socat.sh stunnel-socat.sh RUN chmod +x /docker-entrypoint.sh /stunnel-socat.sh ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["stunnel_thread", "/etc/stunnel/stunnel.conf"]
docker-entrypoint.sh
#!/bin/bash # скрипт инициализации # --------------------------------- # настройка csp echo "Configuring CryptoPro CSP..." # импорт сертификата с закрытым ключом if [[ ! -f "$STUNNEL_CERTIFICATE_PFX_FILE" ]]; then echo "Client certificate not found in ${STUNNEL_CERTIFICATE_PFX_FILE}" exit 1 fi certmgr -install -pfx -file "${STUNNEL_CERTIFICATE_PFX_FILE}" -pin "${STUNNEL_CERTIFICATE_PIN_CODE}" -silent || exit 1 echo "Certificate was imported." echo # определение контейнера-хранилища закрытых ключей containerName=$(csptest -keys -enum -verifyc -fqcn -un | grep 'HDIMAGE' | awk -F'|' '{print $2}' | head -1) if [[ -z "$containerName" ]]; then echo "Keys container not found" exit 1 fi # установка сертификата клиента certmgr -inst -cont "${containerName}" -silent || exit 1 # экспорт сертификата для stunnel exportResult=$(certmgr -export -dest /etc/stunnel/client.crt -container "${containerName}") if [[ ! -f "/etc/stunnel/client.crt" ]]; then echo "Error on export client certificate" echo "$result" exit 1 fi echo "CSP configured." echo # --------------------------------- # запуск socat echo "Starting socat..." nohup bash /stunnel-socat.sh </dev/null >&1 2>&1 & # --------------------------------- # запуск stunnel echo "Configuring stunnel..." sed -i "s/^debug=.*$/debug=$STUNNEL_DEBUG_LEVEL/g" /etc/stunnel/stunnel.conf echo "Starting stunnel" exec "$@"
stunnel-socat.sh
#!/bin/bash echo Configuring socat... socatParameters="TCP:${STUNNEL_HOST}" if [[ -n "$STUNNEL_HTTP_PROXY" ]]; then # если указан http-прокси, подключение будет происходить через него socatParameters="PROXY:${STUNNEL_HTTP_PROXY}:${STUNNEL_HOST},proxyport=${STUNNEL_HTTP_PROXY_PORT}" if [[ -n "$STUNNEL_HTTP_PROXY_CREDENTIALS" ]]; then socatParameters="${socatParameters},proxyauth=${STUNNEL_HTTP_PROXY_CREDENTIALS}" fi fi socatCmd="socat UNIX-LISTEN:/var/run/socat.sock,reuseaddr,fork ${socatParameters}" while true; do rm -f /var/run/socat.sock echo $(date) "Start socat instance." ${socatCmd} sleep 1 done
stunnel.conf
foreground=yes pid=/var/opt/cprocsp/tmp/stunnel_cli.pid output=/dev/stdout debug=5 [https] client=yes accept=80 cert=/etc/stunnel/client.crt verify=0 connect=/var/run/socat.sock
Про Dockerfile рассказывать не буду, он достаточно тривиален, а вот скрипт инициализации docker-entrypoint.sh интереснее. Первым делом скрипт импортирует сертификат с закрытым ключом в хранилище ключей, так как “КриптоПро Stunnel” для работы необходим закрытый ключ. Затем из хранилища экспортируется сертификат с открытым ключом в формате DER. В дальнейшем по этому сертификату “КриптоПро Stunnel” будет получать закрытый ключ из хранилища ключей.
После инициализации хранилища ключей происходит настройка и запуск socat. Для конфигурирования socat добавлены переменные окружения, которые позволяют указать, через какой HTTP-прокси необходимо выполнять запросы. Не буду останавливаться на этих переменных – их описание есть в репозитории. Однако не лишним будет уточнить, что, если переменные не указаны, socat будет самостоятельно выполнять TCP-запросы до целевого сервера. Для получения входящих запросов socat открывает unix-сокет, на который и будет обращаться “КриптоПро Stunnel”.
Финальным шагом в скрипте являются конфигурирование Stunnel и его последующий запуск.
“КриптоПро Stunnel” при запуске начинает прослушивать порт 80, то есть принимать голый HTTP-трафик. HTTP-трафик будет шифроваться по ГОСТу и пересылаться на unix-сокет, который слушает socat. Socat, в свою очередь, откроет соединение с целевым сервером, напрямую или через HTTP-прокси, и отправит уже шифрованный запрос.
Шифрованный ответ от целевого сервера пройдет ту же цепочку, только обратном порядке, и вызывающему приложению будет возвращен ответ в виде plain text, что позволит не реализовывать ГОСТ TLS внутри приложений (если такая реализация вообще возможна).
Вместо заключения
К сожалению, документация по отечественным решениям зачастую достаточно скромна. К примеру, на попытки заставить работать “КриптоПро Stunnel” через HTTP-прокси ушло много времени, пока не пришло понимание, что “КриптоПро Stunnel” прокси не поддерживает и что без еще одного инструмента не обойтись.
Данная статья призвана помочь сберечь ваше время, надеюсь, описанное окажется полезным.
Бонус
В качестве бонуса хотелось бы поделиться несколькими советами:
При выполнении запроса через Stunnel всегда добавляйте HTTP-заголовок “Host: example.service.ru” с указанием целевого сервера. Если заголовок не будет указан, сервер может возвращать код 404, т. к. неясно, к какому домену относится запрос.
Помимо заголовка Host, необходимо передавать заголовок Content-Length, иначе stunnel некорректно обрабатывает запрос.
Об этом часто забывают, но нужно помнить, что URL чувствителен к регистру. Например, https://example.service.ru/url1 не равен https://example.service.ru/URL1, и результат запроса будет зависеть от реализации веб-сервера, в частности “КриптоПро DSS” требователен к регистру.
