Всем привет! Я Максим, бэкенд-разработчик команды 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, т. к. неясно, к какому домену относится запрос.
Об этом часто забывают, но нужно помнить, что URL чувствителен к регистру. Например, https://example.service.ru/url1 не равен https://example.service.ru/URL1, и результат запроса будет зависеть от реализации веб-сервера, в частности “КриптоПро DSS” требователен к регистру.
Список ресурсов
https://www.stunnel.org/static/stunnel.html
https://habr.com/ru/company/aktiv-company/blog/477650/
https://www.cryptopro.ru/forum
man socat