Взаимодействие с ГИС ЖКХ с помощью stunnel и openssl по ГОСТу

в 6:58, , рубрики: open source, openssl, python, stunnel, гис жкх, гост, жкх, криптография, криптопро, системное администрирование

Взаимодействие с ГИС ЖКХ с помощью stunnel и openssl по ГОСТу - 1

Встала перед нами в полный рост задача наладить взаимодействие с ГИС ЖКХ. Согласно документации, предполагается использование небезызвестного отечественного ПО для шифрования туннеля и формирования ЭЦП по ГОСТу, но это не наш метод. Вооружившись гуглом и консолью, я и slavam реализовали необходимый функционал подручными средствами.
Всё необходимое ПО есть как на Linux, так и на Windows платформах, потому методику можно назвать мультиплатформенной.

  1. Подготовка
  2. Криптотуннель по ГОСТу
  3. Формирование XMLDSig
  4. Ссылки

Подготовка

Взаимодействие состоит из следующих частей:

  • Установка по ГОСТу шифрованного туннеля до серверов ГИС ЖКХ.
  • Формирование и отправка подписанного XML запроса.
  • Получение и проверка ответа.

Для работы используется сервер CentOS 6, Python 2.7.11, OpenSSL + GOST engine, stunnel. Краткая инструкция установки openssl с поддержкой ГОСТ

для ленивых:

# wget https://www.openssl.org/source/openssl-1.0.2g.tar.gz
# tar -xvf openssl-1.0.2g.tar.gz
# cd openssl-1.0.2g/
# yum groupinstall "Development Tools"
# yum install zlib zlib-devel
# ./config shared zlib enable-rfc3779
# make && make install
# echo "/usr/local/ssl/lib/" > /etc/ld.so.conf.d/openssl.conf
# ldconfig
# /usr/local/ssl/bin/openssl ciphers | tr ":" "n" | grep -i gost
GOST2001-GOST89-GOST89
GOST94-GOST89-GOST89

# cat /usr/local/ssl/openssl.cnf
…………
openssl_conf = openssl_def

[openssl_def]
engines = engine_section

[engine_section]
gost = gost_section

[gost_section]
engine_id = gost
default_algorithms = ALL
…………

Думаю, ничего сложного быть не должно и всё уже сотни раз описано. А в более свежих дистрибутивах модуль gost есть из коробки.

OpenSSL используется двояко: с ним собран stunnel, что-бы работал по ГОСТу; и утилита openssl вызывается из кода python-а для формирования и проверки подписей. Вызов openssl обусловлен тем, что не удалось из кода python-на задавать используемый крипто-модуль. Всё необходимое ПО есть как на Linux, так и на Windows платформах, потому методику можно назвать мультиплатформенной.

Для python-а сервера CentOS 6 нужно будет обновить библиотеку lxml, а для этого поставить несколько дополнительных пакетов:

# yum install libxml2 libxml2-devel libxslt libxslt-devel python-devel
# pip install lxml --upgrade

Так же нам потребуются файлы сертификата и закрытого ключа в формате PKCS12 (.pem). Получить их из eToken-а можно с помощью утилит вроде P12FromGostCSP или вручную. Если сделать это по каким-либо причинам не получается, то есть вариант работы с ключом «Рутокен ЭЦП» напрямую. На сайте есть подробные инструкции, как обучить этому OpenSSL и stunnel. Таким образом, задача сводится к предыдущей. У меня под рукой такого ключа не оказалось, потому проверить не удалось.

Криптотуннель по ГОСТу

Туннель поднимается с помощью Stunnel, который работает в режиме прокси. Нужно только научить его использовать модуль gost. Но тут есть момент — чтобы модуль инициализировался правильно, нужно слегка исправить исходники. Проблема, как я понял, связана с порядком инициализации модуля и методов. Итак:

# wget https://www.stunnel.org/downloads/stunnel-5.31.tar.gz
# tar -xvf stunnel-5.31.tar.gz
# cd stunnel-5.31

Правим файл src/options.c и в конце функции "NOEXPORT char *engine_init(void)" добавляем вызов SSL_library_init();.

Получится как-то так:

NOEXPORT char *engine_init(void) {
    if(engine_initialized) /* either first or already initialized */
        return NULL; /* OK */
    s_log(LOG_DEBUG, "Initializing engine #%d (%s)",
        current_engine+1, ENGINE_get_id(engines[current_engine]));
    if(!ENGINE_init(engines[current_engine])) {
        if(ERR_peek_last_error()) /* really an error */
            sslerror("ENGINE_init");
        else
            s_log(LOG_ERR, "Engine #%d (%s) not initialized",
                current_engine+1, ENGINE_get_id(engines[current_engine]));
        return "Engine initialization failed";
    }
#if 0
    /* it is a bad idea to set the engine as default for all sections */
    /* the "engine=auto" or "engineDefault" options should be used instead */
    if(!ENGINE_set_default(engines[current_engine], ENGINE_METHOD_ALL)) {
        sslerror("ENGINE_set_default");
        return "Selecting default engine failed";
    }
#endif
    s_log(LOG_INFO, "Engine #%d (%s) initialized",
        current_engine+1, ENGINE_get_id(engines[current_engine]));
    SSL_library_init();
    engine_initialized=1;
    return NULL; /* OK */
}

Решение было найдено тут.
Собираем с подключением ранее собранного OpenSSL:

# ./configure --with-ssl=/usr/local/ssl  --disable-libwrap
# make && make install

Файл конфигурации /etc/stunnel.conf:

client=yes
# сертификат ГИС ЖКХ (тестовый).
CAFile=/etc/crypto/CA-SIT.pem
engine=gost
sslVersion=TLSv1
engineDefault = ALL

output=/var/log/stunnel.log
DEBUG=4

# извлечённые с eToken-а.
cert=/etc/crypto/public.pem
key=/etc/crypto/private.key

[pseudo-https]
# адрес и порт сервера, который будет принимать запросы.
accept = 10.1.5.133:8080
# адрес сервера ГИС ЖКХ, куда прокладываем туннель (тестовый).
connect = 54.76.42.99:60045
ciphers = GOST2001-GOST89-GOST89

Файлы сертификатов кладём в директорию /etc/crypto/. В общем случае, stunnel может работать под любым пользователем, но у нас пусть будет root.

Простейший init.d скрипт управления службой:

#! /bin/bash
#
# stunnel          Start/Stop Stunnel
#
# chkconfig: 2345 90 60
# description: launches Stunnel
# processname: stunnel
# config: /etc/stunnel.conf

# Source function library.
. /etc/init.d/functions

# See how we were called.

prog="Stunnel CryptoTunnel"
RNG=PROGRAM
export RNG

start() {
    echo -n $"Starting $prog: "
    /usr/local/bin/stunnel  /etc/stunnel.conf
    RETVAL=$?
    [ $RETVAL -eq 0 ] && success
    [ $RETVAL -ne 0 ] && failure
    echo
    return $RETVAL
}

stop() {
    echo -n $"Stopping $prog: "
    /usr/bin/killall /usr/local/bin/stunnel >/dev/null 2>&1
    RETVAL=$?
        [ $RETVAL -eq 0 ] && success
        [ $RETVAL -ne 0 ] && failure
        echo
        return $RETVAL
}

restart() {
    stop
    start
}

case "$1" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  restart)
    restart
    ;;
  *)
    echo $"Usage: $0 {start|stop|status|restart}"
    exit 1
esac

Можно запускать и проверять, например,

запросом на адрес сервера:

# curl http://10.1.5.133:8080/ext-bus-nsi-service/services/Nsi?wsdl
<?xml version='1.0' encoding='UTF-8'?><wsdl:definitions xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi-service/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:nsi-common="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi-common/" xmlns:nsi="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi/" xmlns:ns="http://www.w3.org/2000/09/xmldsig#" xmlns:base="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/" targetNamespace="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi-service/">
  <wsdl:types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi-service/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:nsi-common="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi-common/" xmlns:nsi="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi/" xmlns:ns="http://www.w3.org/2000/09/xmldsig#" xmlns:base="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/">

  <xs:import namespace="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/" schemaLocation="http://54.76.42.99:60046//ext-bus-nsi-service/services/Nsi?xsd=hcs-basetypes-8.7.0.7.xsd"/>

  <xs:import namespace="http://dom.gosuslugi.ru/schema/integration/8.7.0.7/nsi/" schemaLocation="http://54.76.42.99:60046//ext-bus-nsi-service/services/Nsi?xsd=hcs-nsi-types-8.7.0.7.xsd"/>

</xs:schema>
………

Если вывод совсем не похож, то внимательно смотрим в лог файл /var/log/stunnel.log.

Формирование XMLDSig

Когда туннель до сервера ГИС ЖКХ настроен и работает, можно слать туда всякие запросы и получать нужные ответы. Запрос отправляется в виде XMLDSig, в котором содержится сам запрос, хеш этого запроса, хеш сертификата, сам сертификат, подпись хеша запроса с хешем сертификата и куча полей, всё это описывающих. Самое сложное как раз было раскрутить всю цепочку и получить XML, который успешно проходит проверку со стороны ГИС ЖКХ. Все подписываемые блоки берутся в каноническом виде, а получаемые подписи и хеш-суммы кодируются в BASE64.

Алгоритм формирования XMLDSig можно реализовать используя любой удобный язык программирования. Мы использовали python 2.7.11, демонстрационный код прилагается. Как пример, буду так же приводить консольный аналог.

0. Из сертификата достаются серийный номер и данные о выпустившем, генерируются необходимые id и запоминается текущее время.
1. С помощью любого soap клиента (например, suds на Python) формируется SOAP-запрос к серверу ГИС ЖКХ.

Пример SOAP-запроса:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
        xmlns:ns0="http://dom.gosuslugi.ru/schema/integration/8.7.0.3/nsi/" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<RequestHeader
        xmlns="http://dom.gosuslugi.ru/schema/integration/8.7.0.4/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Date>2016-04-11T14:28:28</Date>
<MessageGUID>29f93de1-25b6-21e5-24ae-2c6f65dfe2b2</MessageGUID>
<SenderID>4eb0a7d6-6317-45cf-8974-10e75cbb0cbc</SenderID>
</RequestHeader>
</SOAP-ENV:Header>
<ns1:Body>
<ns0:exportDataProviderNsiItemRequest Id="signed-element">
<ns0:RegistryNumber>51</ns0:RegistryNumber></ns0:exportDataProviderNsiItemRequest>
</ns1:Body>
</SOAP-ENV:Envelope>

Где SenderID — идентификатор управляющей компании, от лица которой делается запрос. MessageGUID — уникальный ID, генерируемый как пожелается. Тело <ns1:Body> — сам запрос с дополнительными полями. Id="signed-element" — ID запроса, который указываем, как хотим и на который ориентируемся, подписывая запрос.

2. Берётся содержимое тега <ns1:Body>(точнее часть с Id и без первого и последнего символа перевода строки), канонизируется алгоритмом C14N (exclusive=True), считается от неё хеш-сумма по ГОСТу и выводится в виде BASE64. Получаем digest1. Консольный аналог:

# cat in.xml ; echo
<ns0:exportDataProviderNsiItemRequest Id="signed-element">
<ns0:RegistryNumber>51</ns0:RegistryNumber></ns0:exportDataProviderNsiItemRequest>
# cat in.xml | openssl dgst -engine gost -md_gost94  -binary | base64

* openssl с поддержкой ГОСТ. Engine указан явно, но, настроив openssl.cnf, можно этого и не делать.

3. Берётся сертификат в x509, декодируется из BASE64, считается от него хеш-сумма и вывод кодируется в BASE64. Получаем digest2.

4. Используя полученные данные, формируется содержимое тега <xades:SignedProperties>(см. Шаблон), канонизируется алгоритмом C14N (exclusive=False) и от содержимого считается digest3 (BASE64).

5. Формируется блок <ds:SignedInfo>, канонизируется алгоритмом C14N (exclusive=False), подписывается и кодируется в BASE64. Получается значение блока <ds:SignatureValue>. Консольный аналог:

cat SignedInfo.xml | openssl dgst -sign private.key -engine gost -md_gost94 -binary | base64 

где SignedInfo.xml – уже канонизированный блок, без последнего перевода строки.

6. Все полученные значения вносятся в

Шаблон XML-документа формата XAdES-BES:

<ds:Signature Id="xmldsig-{signature_id}" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411"/>
<ds:Reference Id="xmldsig-{signature_id}-ref0" URI="#{signed_id}">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gostr3411"/>
<ds:DigestValue>{digest1}</ds:DigestValue>
</ds:Reference>
<ds:Reference Type="http://uri.etsi.org/01903#SignedProperties" URI="#xmldsig-{signature_id}-signedprops">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gostr3411"/>
<ds:DigestValue>{digest3}</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue Id="xmldsig-{signature_id}-sigvalue">
{signature_value}
</ds:SignatureValue>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
{x590_cert}
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<ds:Object><xades:QualifyingProperties Target="#xmldsig-{signature_id}" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" xmlns:xades141="http://uri.etsi.org/01903/v1.4.1#"><xades:SignedProperties Id="xmldsig-{signature_id}-signedprops"><xades:SignedSignatureProperties><xades:SigningTime>{signing_time}</xades:SigningTime><xades:SigningCertificate><xades:Cert><xades:CertDigest><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gostr3411"/><ds:DigestValue>{digest2}</ds:DigestValue></xades:CertDigest><xades:IssuerSerial><ds:X509IssuerName>{x509_issuer_name}</ds:X509IssuerName><ds:X509SerialNumber>{x509_sn}</ds:X509SerialNumber></xades:IssuerSerial></xades:Cert></xades:SigningCertificate></xades:SignedSignatureProperties></xades:SignedProperties></xades:QualifyingProperties></ds:Object>
</ds:Signature>

Этот шаблон удалось сформировать расковыряв и проанализировав комплекс, который рекомендует использовать ГИС ЖКХ. Сам он находится в свободном доступе, но для работы требует СКЗИ КриптоПро CSP и СКЗИ Trusted Java 2.0.

7. Для того, чтобы проверить сформированную таким образом подпись, необходимо проделать все действия в обратном порядке. Консольный вариант проверки подписи:

# cat SignedInfo.xml | openssl dgst -engine gost -md_gost94 -verify <(openssl x509 -engine gost -in public.pem -pubkey -noout) -signature signature.sig

где signature.sig – раскодированная из BASE64 подпись, а SignedInfo.xml проверяемый блок <ds:SignedInfo>...</ds:SignedInfo> целиком. Значения хеш-сумм просто сравниваются.

Демонстрационный код на python-е можно взять и использовать (на свой страх и риск) отсюда. Автор кода — Вячеслав ( slavam, RO). Подобный алгоритм можно реализовать средствами любого удобного языка, без необходимости покупки каких-либо дополнительных средств и компонентов. Вызов утилит OpenSSL из кода хоть и выглядит топорным, зато работает как на Linux так и на Windows платформе и позволяет отказаться от использования КриптоПро и дополнительных компонентов.
Система взаимодействия с ГИС ЖКХ у нас ещё на стадии создания, но получаемый XMLDSig проходит необходимые проверки.
Надеемся, эта статья облегчит кому-нибудь задачу по реализации подобного велосипеда.

Спасибо за внимание.

Cсылки

Автор: Lelik13a

Источник

  1. Александр:

    Спасибо друг!!

  2. Roman:

    Спасибо!

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js