Реализация описана для PHP, но подходит для всех.
Конфиги
Начнём с контейнера, из которого будем общаться с ГИС ЖКХ. Тут приведён конфиг контейнера с продакшена, поэтому есть лишние (для вас) пакеты
Пока просто посмотрим, пояснения будут после кода
FROM php:8.1-fpm-alpine
# это не критично, но мне нравится zsh
RUN apk add zsh
# пакеты для работы openssl
RUN apk add \
git \
alpine-sdk \
cmake \
wget \
bash \
libxml2-dev \
libssl1.1
# пакеты для работы с zip-архивами; будут нужны для работы с xml-файлами
RUN apk add \
libzip-dev \
zip \
unzip
# пакеты для использования gd
RUN apk add \
libpng \
libpng-dev \
freetype-dev
# postgres
RUN apk add \
postgresql-dev
# общение с oracle
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions
RUN install-php-extensions oci8
# кофигурируем php
RUN docker-php-ext-configure zip
RUN docker-php-ext-configure gd --with-freetype
RUN docker-php-ext-install soap pdo pdo_pgsql zip bcmath gd intl
# composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php composer-setup.php
RUN php -r "unlink('composer-setup.php');"
RUN mv composer.phar /usr/local/bin/composer
# установка движка ГОСТ для openssl
WORKDIR /opt
COPY docker/production/php/openssl_gost.conf /opt/openssl_gost.conf
RUN git clone --branch=openssl_1_1_1 https://github.com/gost-engine/engine.git gost-engine
WORKDIR /opt/gost-engine
RUN mkdir build
WORKDIR /opt/gost-engine/build
RUN cmake -DCMAKE_BUILD_TYPE=Release -DOPENSSL_ROOT_DIR=/usr/ssl -DOPENSSL_LIBRARIES=/usr/ssl/lib -DOPENSSL_ENGINES_DIR=/usr/ssl/lib/engines-3 ..
RUN cmake --build . --config Release
RUN cmake --build . --target install --config Release
# на моей машине openssl лежит в папке /etc/ssl, на проде в папке /etc/ssl1.1 - не стал разбираться почему
RUN OPENSSL_DIRECTORY="$([[ -d /etc/ssl1.1 ]] && echo '/etc/ssl1.1' || echo '/etc/ssl')" && \
sed -i '1s;^;openssl_conf = openssl_def\n;' "$OPENSSL_DIRECTORY/openssl.cnf" && \
cat /opt/openssl_gost.conf >> "$OPENSSL_DIRECTORY/openssl.cnf"
# ставим сертификаты для stunnel
COPY docker/production/php/stunnel.conf /etc/stunnel.conf
RUN mkdir -p /etc/crypto
# публичный ключ стенда ГИС ЖКХ
COPY gis/certs/ca-ppak.pem /etc/crypto/ca-ppak.pem
# публичный ключ вашего сертификата
COPY gis/certs/certificate.pem /etc/crypto/certificate.pem
# приватный ключ вашего сертификата
COPY gis/certs/private.key /etc/crypto/private.key
RUN chmod -R 700 /etc/crypto
# ставим сам stunnel
WORKDIR /opt
RUN wget https://www.stunnel.org/downloads/stunnel-5.66.tar.gz -O stunnel.tar.gz
RUN tar -xvf stunnel.tar.gz
WORKDIR /opt/stunnel-5.66
RUN sed -i '4745 i SSL_library_init();' src/options.c
RUN ./configure --disable-libwrap
RUN make && make install
# бутстрапим cron
COPY docker/production/php/crontab /opt/crontab
RUN chmod 0644 /opt/crontab && crontab /opt/crontab
RUN crond
WORKDIR /app
# запускаем все нужные процессы
COPY docker/production/php/startup.sh /opt/startup.sh
RUN chmod +x /opt/startup.sh
CMD ["/opt/startup.sh"]
На строке 47 ссылаюсь на openssl_gost.conf, вот он:
# gost support
[openssl_def]
engines = engine_section
[engine_section]
gost = gost_section
[gost_section]
engine_id = gost
dynamic_path = /usr/ssl/lib/engines-3/gost.so
default_algorithms = ALL
CRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet
На строке 89 ссылаюсь на /opt/startup.sh, вот он:
stunnel /etc/stunnel.conf
php-fpm
Где stunnel.conf это:
socket=lsocket=l:TCP_NODELAY=1
socket=r:TCP_NODELAY=1
CAFile=/etc/crypto/ca-ppak.pem
engine=gost
sslVersion=TLSv1
engineDefault=ALL
output=/var/log/stunnel.log
DEBUG=7
client=yes
[pseudo-https]
accept=127.0.0.1:3000
connect=api.dom.gosuslugi.ru:443
ciphers=GOST2012-GOST8912-GOST8912
verify=0
TIMEOUTclose=0
cert=/etc/crypto/certificate.pem
key=/etc/crypto/private.keyTCP_NODELAY=1socket=r:TCP_NODELAY=1CAFile=/etc/crypto/ca-ppak.pemengine=gostsslVersion=TLSv1engineDefault=ALLoutput=/var/log/stunnel.logDEBUG=7client=yes[pseudo-https]accept=127.0.0.1:3000connect=api.dom.gosuslugi.ru:443ciphers=GOST2012-GOST8912-GOST8912verify=0TIMEOUTclose=0cert=/etc/crypto/certificate.pemkey=/etc/crypto/private.key
Примечания к конфигам
-
Пути до файлов, указанные относительно корня проекта:
docker/production/php - путь до Dockerfile контейнера
gis/certs - путь до папок с сертификатом
В вашем контейнере, с которого вы будете делать запросы должен быть openssl v1.1.1, с версией 3.0 у меня нет времени разбираться) может добрые люди в комментах расскажут как это делается
Если контейнер не запускается и ругается на то, что не может найти stunnel, то поднимите версию, чекнув текущую на официальном сайте. К сожалению, у них нет ссылки на самую свежую стабильную версию и если выходит новая версия, то ссылки со старыми начинают выдавать 404.
Примечания к сертификатам
Нам нужны открытый и закрытый ключ. Из КриптоПРО можно выгрузить сертификат в формате cert.000
. После этого идём сюда и, следуя инструкциям, делаем себе приватный ключ - https://github.com/ddruganov/get-cpcert. Публичный ключ можно достать через openssl x509 -in cert.crt -out cert.pem -outform PEM
Примечание к криптотуннелю
Для того, чтобы проксировать все запросы через криптотуннель, в php нужно допилить SoapClient:
final class CustomSoapClient extends SoapClient
{
private Closure $requestHandlerCallback;
public function __construct(string $wsdl, Closure $requestHandlerCallback)
{
$this->requestHandlerCallback = $requestHandlerCallback;
parent::__construct($wsdl, [
'trace' => true,
'exceptions' => false,
'use' => SOAP_LITERAL,
'style' => SOAP_DOCUMENT,
]);
}
public function __doRequest(string $request, string $location, string $action, int $version, bool $oneWay = false): ?string
{
$request = ($this->requestHandlerCallback)($request);
$location = str_replace('https://api.dom.gosuslugi.ru', Yii::$app->params['gis']['tlsTunnelAddress'], $location);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $location,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $request,
CURLOPT_CONNECTTIMEOUT => 20,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
"SOAPAction: $action"
]
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}
В этом коде самое важное это строка 21, где в запросе по soap идёт замена прямого адреса ГИС ЖКХ на адрес криптотуннеля
Заключение
Запуская этот контейнер вы получаете готовое решение, в котором настроен криптотуннель и openssl+gost и в принципе, это всё что требуется, чтобы начать работать с ГИС ЖКХ
Мой подход уже успешно работает на продакшене около 10 месяцев, обрабатывая до 60к запросов в месяц с крайне вариативной по дням загрузкой, за это время сбоев не было (хотя вообще-то откуда им быть, это не бизнес-логика)
Ещё одна важная часть всего взаимодействия это подпись запросов по XMLDSig при помощи полученных сертификатов, но об этом чуть позже, так как там больше прикладного кода и неплохо бы его оформить в репозиторий для наглядности.
Если есть примечания или что-то непонятно - обязательно пишите, я обновлю гайд и всё поясню. Я сам долго всё это собирал, я знаю, что огромное количество людей страдает с этой интеграцией постоянно (https://gitter.im/springjazzy/GIS_JKH_Integration)
Комментарии (4)
niyazm524
16.01.2023 11:09+1И где ты был 5 месяцев назад, когда я сквозь кровь и пот пытался добиться работоспособности всей этой схемы...
Особенно меня тогда протестировало то, что делалось всё на Node.JS, и пришлось хорошенько погрузиться в то, как работает подпись XML, чтобы модифицировать исходный код библиотек
Хорошо, что всё это уже позади.
ddruganov Автор
16.01.2023 11:10слушай, я на самом деле ждал с начала лета, когда можно будет рассказать, начальство не особо это одобряло)
я тоже очень рад, что всё позади :D
svkreml
А можно уточнить: здесь используется несертифицированное решение для подписания, это какие-то тестовые стреды, как планируется прохождение сертификации для конечного решения?
Просто вроде как ГОСТ криптографию предполагается использовать только с криптопро, випнет и там ещё какие-то были сертифицированные криптопровадеры.
ddruganov Автор
это решение работает на боевом стенде, тут проблем нет