Это ода данному посту.

Реализация описана для 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=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.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)