СМЭВ 3. Электронная подпись сообщений на Java и КриптоПро

  • Tutorial


Система межведомственного электронного взаимодействия (СМЭВ) задумывалась как цифровая среда предоставления услуг и исполнения государственных и муниципальных функций в электронной форме.

В настоящее время СМЭВ продолжает расширять свои возможности и вовлекать все большее количество участников взаимодействия.

Что оказалось как нельзя кстати, в том числе для коммерческих организаций, в частности банков, которые все больше стремятся перевести свои услуги в цифру и сериализовать процессы.

В этой статье мы поговорим о том, как своими силами подписать запросы и проверить электронные подписи ответов СМЭВ версии 3.0, и о паре интересных нюансов, с которыми пришлось при этом столкнуться.

Здравствуйте!

Может возникнуть вопрос. Почему своими силами? Когда для СМЭВ 3 есть целый Технологический портал, где

  • опубликована вся документация и методические указания,
  • есть раздел с часто задаваемыми вопросами,
  • можно скачать актуальную версию библиотек клиента СМЭВ 3,
  • предоставлены примеры полных конвертов сообщений с подписями,
  • можно даже проверить онлайн свое сообщение или из примера на соответствие схемам сервиса СМЭВ и на предмет валидности его электронной подписи

Все верно, портал, безусловно, крайне полезный, и всеми его подсказками и инструментами можно и нужно пользоваться, но вот код на Java напишем свой.

По той простой причине, что уже есть собственная информационная система, работающая с форматами электронной подписи XMLDSig, XAdES, в которой применяются библиотеки проекта Apache Santuario, реализующие основные стандарты безопасности для XML. А также библиотеки, входящие в состав КриптоПро JCSP, помимо работы с XML, обеспечивающие API криптографических функций СКЗИ КриптоПро CSP.

Написание собственных методов для работы с электронными подписями СМЭВ 3 в данном случае выглядит более целесообразно, нежели разворачивание полного клиента поставляемого:
ФГБУ НИИ «Восход» (до 21 марта 2016 года ФГУП НИИ «Восход») или интеграция, его отдельных классов и пакетов.

В то же время заглянуть в открытый код клиента всегда полезно, а его наличие само по себе говорит о зрелости системы и высоком уровне поддержки.

Анализ исходных данных


Загружаем с портала СМЭВ 3:


Если уже умеем формировать обычный XMLDSig или подписывать, например, конверты сообщений СМЭВ 2, то больше всего начинает интересовать, чем же отличается конверт с подписью СМЭВ 3 от СМЭВ 2.

Открываем пример конверта СМЭВ 3 SendRequestRequestNoAttach.xml

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/1.1">
   <S:Body>
      <ns2:SendRequestRequest xmlns:ns3="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/faults/1.1" xmlns:ns2="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/1.1" xmlns="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/basic/1.1">
         <ns:SenderProvidedRequestData Id="SIGNED_BY_CONSUMER" xmlns="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/1.1" xmlns:ns="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/1.1" xmlns:ns2="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/basic/1.1">	<ns:MessageID>db0486d0-3c08-11e5-95e2-d4c9eff07b77</ns:MessageID><ns2:MessagePrimaryContent><ns1:BreachRequest xmlns:ns1="urn://x-artefacts-gibdd-gov-ru/breach/root/1.0"  xmlns:ns2="urn://x-artefacts-gibdd-gov-ru/breach/commons/1.0"  xmlns:ns3="urn://x-artefacts-smev-gov-ru/supplementary/commons/1.0.1" Id="PERSONAL_SIGNATURE"> <ns1:RequestedInformation> <ns2:RegPointNum>Т785ЕС57</ns2:RegPointNum> </ns1:RequestedInformation> <ns1:Governance> <ns2:Name>ГИБДД РФ</ns2:Name> <ns2:Code>GIBDD</ns2:Code> <ns2:OfficialPerson> <ns3:FamilyName>Загурский</ns3:FamilyName> <ns3:FirstName>Андрей</ns3:FirstName> <ns3:Patronymic>Петрович</ns3:Patronymic> </ns2:OfficialPerson></ns1:Governance> </ns1:BreachRequest> </ns2:MessagePrimaryContent>	<ns:TestMessage/></ns:SenderProvidedRequestData>
         <ns2:CallerInformationSystemSignature><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411"/><ds:Reference URI="#SIGNED_BY_CONSUMER"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:Transform Algorithm="urn://smev-gov-ru/xmldsig/transform"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gostr3411"/><ds:DigestValue>/jXl70XwnttJB5sSokwh8SaVHwo2gjgILSu0qBaLUAo=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>J3746ks34pOcPGQpKzc0sz3n9+gjPtzZbSEEs4c3sTwbtfdaY7N/hxXzEIvXc+3ad9bc35Y8yBhZ/BYbloGt+Q==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIBcDCCAR2gAwIBAgIEHVmVKDAKBgYqhQMCAgMFADAtMRAwDgYDVQQLEwdTWVNURU0xMQwwCgYDVQQKEwNJUzIxCzAJBgNVBAYTAlJVMB4XDTE1MDUwNzEyMTUzMFoXDTE4MDUwNjEyMTUzMFowLTEQMA4GA1UECxMHU1lTVEVNMTEMMAoGA1UEChMDSVMyMQswCQYDVQQGEwJSVTBjMBwGBiqFAwICEzASBgcqhQMCAiMBBgcqhQMCAh4BA0MABEDoWGZlTUWD43G1N7TEm14+QyXrJWProrzoDoCJRem169q4bezFOUODcNooQJNg3PtAizkWeFcX4b93u8fpVy7RoyEwHzAdBgNVHQ4EFgQUaRG++MAcPZvK/E2vR1BBl5G7s5EwCgYGKoUDAgIDBQADQQCg25vA3RJL3kgcJhVOHA86vnkMAtZYr6HBPa7LpEo0HJrbBF0ygKk50app1lzPdZ5TtK2itfmNgTYiuQHX3+nE</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature></ns2:CallerInformationSystemSignature>
      </ns2:SendRequestRequest>
   </S:Body>
</S:Envelope>


Дедуктивным методом выясняется что:
  • больше не используется прием с выносом из содержимого тега Signature в Security заголовок элемента BinarySecurityToken с сертификатом открытого ключа проверки электронной подписи и ссылкой на него через SecurityTokenReference в теле самого Signature, как, например, в СМЭВ 2.4.6. Теперь сертификат должен находиться внутри Signature.
  • второе и, по сути, самое существенное и важное изменение, оказывающее большое влияние на процесс подписи — это добавление новой проприетарной трансформации:

    <ds:Transform Algorithm="urn://smev-gov-ru/xmldsig/transform"/>


Через эту трансформацию распространяются собственные правила каноникализации СМЭВ 3.

Каноникализация — процесс приведения данных, имеющих несколько возможных форм представления, к одному нормализованному стандартному виду.

Перед тем как посчитать хэш подписываемого атрибута в XML-конверте и подписать, необходимо выполнить его конвертацию в заданный правилами СМЭВ 3 вид.

В поисках описания трансформации urn://smev-gov-ru/xmldsig/transform открываем Методические рекомендации 3.4.0.3

Знакомимся с пунктом 4.4.2.1 Правила формирования электронной подписи сообщений

Формат подписи XMLDSig detached (https://www.w3.org/TR/xmldsig-core/)

Трансформация, дополнительно к канонизации urn://smev-gov-ru/xmldsig/transform

Требования к форматированию В XML-структуре подписи между элементами не допускается наличие текстовых узлов, в том числе переводов строки.

Пункт Методических указаний 12.4. ПРИЛОЖЕНИЕ 4: ОБРАЗЦОВАЯ РЕАЛИЗАЦИЯ ТРАНСФОРМАЦИИ URN://SMEV-GOV-RU/XMLDSIG/TRANSFORM
содержит Java класс SmevTransformSpi.java, реализующий алгоритм трансформации «urn://smev-gov-ru/xmldsig/transform», наследник org.apache.xml.security.transforms.TransformSpi из библиотеки Apache Santuario.

Таким образом, чтобы обеспечить каноникализацию подписываемого конверта СМЭВ 3, можно использовать в своем коде этот класс трансформации.

Единственным условием и ограничением в этом случае будет, что для обработки XML-документа при формировании подписи или ее проверки нужно использовать именно org.apache.xml.security.signature.XMLSignature из проекта Apache Santuario.

Задействовать инструменты из пакетов javax.xml.crypto.dsig или ru.CryptoPro.JCPxml.xmldsig просто так уже не получится.

Подготовка к подписи по правилам СМЭВ 3


Apache Santuario изначально ничего не знает про ГОСТ криптографические алгоритмы и СКЗИ КриптоПро.

В библиотеке xmlsec-1.5.0.jar в файле \org\apache\xml\security\resource\config.xml содержатся настройки только для работы с зарубежными криптографическими алгоритмами.

Чтобы он начал распознавать и применять ГОСТ, нужно выполнить его инициализацию.

По старинке это делалось так:

//APACHE-SANTUARIO INIT WITH CryptoPro JCP
        System.setProperty("org.apache.xml.security.resource.config", "resource/jcp.xml");
        org.apache.xml.security.Init.init();
        String cfile1 = System.getProperty("org.apache.xml.security.resource.config");
        LOGGER.log(Level.INFO, "Init class URL: " + org.apache.xml.security.Init.class.getProtectionDomain().getCodeSource().getLocation());
        LOGGER.log(Level.INFO, cfile1);

В новых версиях КриптоПро JCP (JCSP) инициализацию выполнит одна строчка:

ru.CryptoPro.JCPxml.xmldsig.JCPXMLDSigInit.init();

Теперь нужно Apache Santuario научить новым правилам трансформации, которые диктует СМЭВ 3. Для этого регистрируем класс трансформации:

  try {
                Transform.register(SmevTransformSpi.ALGORITHM_URN, SmevTransformSpi.class.getName());
                santuarioIgnoreLineBreaks(true);
                LOGGER.log(Level.INFO, "SmevTransformSpi has been initialized");
            } catch (AlgorithmAlreadyRegisteredException e) {
                LOGGER.log(Level.INFO, "SmevTransformSpi Algorithm already registered: " + e.getMessage());
            } 

Заодно сразу выполняем требование из Методических указаний:

Требования к форматированию В XML-структуре подписи между элементами не допускается наличие текстовых узлов, в том числе переводов строки.

santuarioIgnoreLineBreaks(true);

    private static final String IGNORE_LINE_BREAKS_FIELD = "ignoreLineBreaks";

/**
     * Apache Santuario privileged switch IgnoreLineBreaks property
     * 
     * @param mode
     */
    private void santuarioIgnoreLineBreaks(Boolean mode) {
        try {
            Boolean currMode = mode;
            AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
                
                public Boolean run() throws Exception {
                    Field f = XMLUtils.class.getDeclaredField(IGNORE_LINE_BREAKS_FIELD);
                    f.setAccessible(true);
                    f.set(null, currMode);
                    return false;
                }
            });
            
        } catch (Exception e) {
            LOGGER.warning("santuarioIgnoreLineBreaks " + ExceptionUtils.getFullStackTrace(e));
        }
    }

Делается это в привилегированном блоке AccessController.doPrivileged
и через reflection, из-за особенности реализации свойства ignoreLineBreaks в Santuario.

Просто через настройку системного свойства:

System.setProperty("org.apache.xml.security.ignoreLineBreaks", "true");

не работает.

Через настройку опции JVM:

-Dcom.sun.org.apache.xml.internal.security.ignoreLineBreaks=true

работает.

Если взглянуть на код класса org.apache.xml.security.utils.XMLUtils, то можно увидеть, что поле ignoreLineBreaks статическое, инициализируется в привилегированном блоке из системного свойства «org.apache.xml.security.ignoreLineBreaks».

private static boolean ignoreLineBreaks =
        AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
            public Boolean run() {
                return Boolean.valueOf(Boolean.getBoolean
                    ("org.apache.xml.security.ignoreLineBreaks"));
            }
        }).booleanValue();
public static boolean ignoreLineBreaks() {
        return ignoreLineBreaks;
    }

Такая реализация приводит к невозможности гибко настроить в одном Java процессе для части методов игнорировать перевод строк, а для другой части не игнорировать.

Т.е., если одно приложение выполняет подписи XMLDsig, СМЭВ 2 и СМЭВ 3, все XML документы, обработанные Santuario должны на выходе лишиться перевода строк.

С этим свойством, конечно, возникает вопрос к Apache Santuario:



Подпись сообщений СМЭВ 3


Для подписи документов СМЭВ 3 все готово.

Код подписания выглядит следующим образом:

private static final String XMLDSIG_MORE_GOSTR34102001_GOSTR3411 = "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411";
    private static final String XMLDSIG_MORE_GOSTR3411 = "http://www.w3.org/2001/04/xmldsig-more#gostr3411";
    private static final String CANONICALIZATION_METHOD = "http://www.w3.org/2001/10/xml-exc-c14n#";
    private static final String DS_SIGNATURE = "//ds:Signature";
    private static final String SIG_ID = "sigID";
    private static final String COULD_NOT_FIND_XML_ELEMENT_NAME = "ERROR! Could not find xmlElementName = ";
    private static final String GRID = "#";
    private static final String XML_SIGNATURE_ERROR = "xmlDSignature ERROR: ";

try {
            // инициализация объекта чтения XML-документа
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            
            // установка флага, определяющего игнорирование пробелов в
            // содержимом элементов при обработке XML-документа
            dbf.setIgnoringElementContentWhitespace(true);
            
            // установка флага, определяющего преобразование узлов CDATA в
            // текстовые узлы при обработке XML-документа
            dbf.setCoalescing(true);
            
            // установка флага, определяющего поддержку пространств имен при
            // обработке XML-документа
            dbf.setNamespaceAware(true);
            
// загрузка содержимого подписываемого документа на основе
            // установленных флагами правил из массива байтов data            DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
         
           Document doc = documentBuilder.parse(new ByteArrayInputStream(data));
            
            /*
             * Добавление узла подписи <ds:Signature> в загруженный XML-документ
             */
            
            // алгоритм подписи (ГОСТ Р 34.10-2001)
            final String signMethod = XMLDSIG_MORE_GOSTR34102001_GOSTR3411;
            
            // алгоритм хеширования, используемый при подписи (ГОСТ Р 34.11-94)
            final String digestMethod = XMLDSIG_MORE_GOSTR3411;
            
            final String canonicalizationMethod = CANONICALIZATION_METHOD;
            
         
            String[][] filters = {{XPath2FilterContainer.SUBTRACT, DS_SIGNATURE}};
            String sigId = SIG_ID;
                
                // инициализация объекта формирования ЭЦП в соответствии с
                // алгоритмом ГОСТ Р 34.10-2001
                XMLSignature sig = new XMLSignature(doc, "", signMethod, canonicalizationMethod);
                
                // определение идентификатора первого узла подписи
               
                sig.setId(sigId);
                
                // получение корневого узла XML-документа
                Element anElement = null;
                if (xmlElementName == null) {
                    anElement = doc.getDocumentElement();
                } else {
                    NodeList nodeList = doc.getElementsByTagName(xmlElementName);
                    anElement = (Element) nodeList.item(0);
                }
                // = doc.getElementById("#AppData");
                // добавление в корневой узел XML-документа узла подписи
                if (anElement != null) {
                    anElement.appendChild(sig.getElement());
                } else {
                    throw new SignatureProcessorException(COULD_NOT_FIND_XML_ELEMENT_NAME + xmlElementName);
                }
                
                /*
                 * Определение правил работы с XML-документом и добавление в узел подписи этих
                 * правил
                 */
                
                // создание узла преобразований <ds:Transforms> обрабатываемого
                // XML-документа
                Transforms transforms = new Transforms(doc);
                
                // добавление в узел преобразований правил работы с документом
                // transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
                transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
                transforms.addTransform(SmevTransformSpi.ALGORITHM_URN);
                
                // добавление в узел подписи ссылок (узла <ds:Reference>),
                // определяющих правила работы с
                // XML-документом (обрабатывается текущий документ с заданными в
                // узле <ds:Transforms> правилами
                // и заданным алгоритмом хеширования)
                sig.addDocument(xmlElementID == null ? "" : GRID + xmlElementID, transforms, digestMethod);
                
                /*
                 * Создание подписи всего содержимого XML-документа на основе закрытого ключа,
                 * заданных правил и алгоритмов
                 */
                
                // создание внутри узла подписи узла <ds:KeyInfo> информации об
                // открытом ключе на основе
                // сертификата
                sig.addKeyInfo(x509Cert);
                
                // создание подписи XML-документа
                sig.sign(privateKey);
            
            // определение потока, в который осуществляется запись подписанного
            // XML-документа
            bais = new ByteArrayOutputStream();
            
            // инициализация объекта копирования содержимого XML-документа в
            // поток
            TransformerFactory tf = TransformerFactory.newInstance();
            
            // создание объекта копирования содержимого XML-документа в поток
            Transformer trans = tf.newTransformer();
            
            // копирование содержимого XML-документа в поток
            trans.transform(new DOMSource(doc), new StreamResult(bais));
            bais.close();
        } catch (TransformationException e) {
            throw new SignatureProcessorException("TransformationException " + XML_SIGNATURE_ERROR + e.getMessage());
        } catch (XMLSignatureException e) {
            throw new SignatureProcessorException("XMLSignatureException " + XML_SIGNATURE_ERROR + e.getMessage());
        } catch (TransformerException e) {
            throw new SignatureProcessorException("TransformerException " + XML_SIGNATURE_ERROR + e.getMessage());
        } catch (IOException e) {
            throw new SignatureProcessorException("IOException " + XML_SIGNATURE_ERROR + e.getMessage());
        } catch (XMLSecurityException e) {
            throw new SignatureProcessorException("XMLSecurityException " + XML_SIGNATURE_ERROR + e.getMessage());
        } catch (SAXException e) {
            throw new SignatureProcessorException("SAXException " + XML_SIGNATURE_ERROR + e.getMessage());
        } catch (ParserConfigurationException e) {
            throw new SignatureProcessorException(
                    "ParserConfigurationException " + XML_SIGNATURE_ERROR + e.getMessage());
        }
        return bais.toByteArray();

Основными параметрами здесь являются:

byte[] data, // XML сообщение в виде массива байтов

String xmlElementName, // имя элемента в XML вместе с префиксом, в который следует добавить подпись, для СМЭВ-3 в общем случае "ns2:CallerInformationSystemSignature"

String xmlElementID // ID элемента в XML (если присутствует) вместе с префиксом, на который следует поставить подпись, для СМЭВ-3 в общем случае "SIGNED_BY_CONSUMER"

X509Certificate certificate // сертификат открытого ключа проверки подписи

PrivateKey privateKey // закрытый ключ подписи


Проверка подписи сообщения СМЭВ 3


Код проверки подписи выглядит следующим образом:

private static final QName QNAME_SIGNATURE = new QName("http://www.w3.org/2000/09/xmldsig#", "Signature", "ds");
    private static final String SIGNATURE_NOT_FOUND = "Signature not found!";
    private static final String SIGNATURE_NOT_VALID = "Signature not valid";
    private static final String SMEV_SIGNATURE_PASSED_CORE_VALIDATION = "SmevSignature passed core validation";
    private static final String VERIFY_SIGNATURE_ON_XML_IO_EXCEPTION = "Verify signature on XML IOException: ";
    private static final String VERIFY_SIGNATURE_ON_XML_PARSER_CONFIGURATION_EXCEPTION = "Verify signature on XML ParserConfigurationException: ";
    private static final String VERIFY_SIGNATURE_ON_XML_SAX_EXCEPTION = "Verify signature on XML SAXException: ";
    private static final String VERIFY_SIGNATURE_ON_XML_XML_SIGNATURE_EXCEPTION = "Verify signature on XML XMLSignatureException: ";
    private static final String VERIFY_SIGNATURE_ON_XML_XML_SECURITY_EXCEPTION = "Verify signature on XML XMLSecurityException: ";
    private static final String ID = "Id";

   boolean coreValidity = true;
        try {
            DocumentBuilderFactory bf = DocumentBuilderFactory.newInstance();
            bf.setNamespaceAware(true);
            DocumentBuilder b = bf.newDocumentBuilder();
            Document doc = b.parse(new InputSource(new ByteArrayInputStream(signedXmlData)));
            
            NodeList sigs = doc.getElementsByTagNameNS(QNAME_SIGNATURE.getNamespaceURI(), QNAME_SIGNATURE.getLocalPart());
            org.apache.xml.security.signature.XMLSignature sig = null;
            sigSearch: {
                for (int i = 0; i < sigs.getLength(); i++) {
                    Element sigElement = (Element) sigs.item(i);
                    String sigId = sigElement.getAttribute(ID);
                    if (sigId != null) {
                        sig = new org.apache.xml.security.signature.XMLSignature(sigElement, "");
                        break sigSearch;
                    }
                }
                throw new XMLSignatureVerificationException(SIGNATURE_NOT_FOUND);
            }
            org.apache.xml.security.keys.KeyInfo ki = (org.apache.xml.security.keys.KeyInfo) sig.getKeyInfo();
            
            X509Certificate certificate = ki.getX509Certificate();
            
            if (!sig.checkSignatureValue(certificate.getPublicKey())) {
                coreValidity = false;
                LOGGER.log(Level.INFO, SIGNATURE_NOT_VALID);
            } else {
                LOGGER.log(Level.INFO, String.format(SMEV_SIGNATURE_PASSED_CORE_VALIDATION));
            }
            
        } catch (IOException e) {
            throw new XMLSignatureVerificationException(VERIFY_SIGNATURE_ON_XML_IO_EXCEPTION + ExceptionUtils.getStackTrace(e));
        } catch (ParserConfigurationException e) {
            throw new XMLSignatureVerificationException(VERIFY_SIGNATURE_ON_XML_PARSER_CONFIGURATION_EXCEPTION + ExceptionUtils.getStackTrace(e));
        } catch (SAXException e) {
            throw new XMLSignatureVerificationException(VERIFY_SIGNATURE_ON_XML_SAX_EXCEPTION + ExceptionUtils.getStackTrace(e));
        } catch (org.apache.xml.security.signature.XMLSignatureException e) {
            throw new XMLSignatureVerificationException(VERIFY_SIGNATURE_ON_XML_XML_SIGNATURE_EXCEPTION + ExceptionUtils.getStackTrace(e));
        } catch (XMLSecurityException e) {
            throw new XMLSignatureVerificationException(VERIFY_SIGNATURE_ON_XML_XML_SECURITY_EXCEPTION + ExceptionUtils.getStackTrace(e));
        }
        
        return coreValidity;


Проблемы. Хэш не совпадает


Внимание!



Для отладки использовался пример конверта СМЭВ 3 SendRequestRequestNoAttach.xml
Из него был удален элемент ds:Signature с целью подписать сообщение заново и сверить с оригиналом.

Несмотря на то, что метод подписи и трансформация SmevTransformSpi, взятая из Методических указаний, отрабатывали, на выходе был подписанный документ, подпись которого при онлайн-проверке на портале СМЭВ 3 трактовалась как

ЭП-ОВ не подтверждена: Ошибка проверки ЭП: Нарушена целостность ЭП

Почему

<ds:DigestValue>e76oVeYGapFDE+PV6glsj0XDjLHydLMd0cSkFPY8fWk=</ds:DigestValue>

не совпадал с оригинальным примером:

<ds:DigestValue>/jXl70XwnttJB5sSokwh8SaVHwo2gjgILSu0qBaLUAo==</ds:DigestValue>

Для диагностики причин в класс SmevTransformSpi в метод process был добавлен свой XMLEventWriter.

ByteArrayOutputStream baos = new ByteArrayOutputStream();
XMLEventWriter bdst =
outputFactory.get().createXMLEventWriter(baos, ENCODING_UTF_8); 

для параллельного анализа всех этапов трансформации.

Нормализованный элемент XML, на который требуется поставить подпись, выглядел следующим образом:

<ns1:SenderProvidedRequestData xmlns:ns1="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/1.1" Id="SIGNED_BY_CONSUMER"><ns1:MessageID>db0486d0-3c08-11e5-95e2-d4c9eff07b77</ns1:MessageID><ns2:MessagePrimaryContent xmlns:ns2="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/basic/1.1"><ns3:BreachRequest xmlns:ns3="urn://x-artefacts-gibdd-gov-ru/breach/root/1.0" Id="PERSONAL_SIGNATURE"><ns3:RequestedInformation><ns4:RegPointNum xmlns:ns4="urn://x-artefacts-gibdd-gov-ru/breach/commons/1.0">Т785ЕС57</ns4:RegPointNum></ns3:RequestedInformation><ns3:Governance><ns4:Name>ГИБДД РФ</ns4:Name><ns4:Code>GIBDD</ns4:Code><ns4:OfficialPerson><ns5:FamilyName xmlns:ns5="urn://x-artefacts-smev-gov-ru/supplementary/commons/1.0.1">Загурский</ns5:FamilyName><ns5:FirstName>Андрей</ns5:FirstName><ns5:Patronymic>Петрович</ns5:Patronymic></ns4:OfficialPerson></ns3:Governance></ns3:BreachRequest></ns2:MessagePrimaryContent><ns1:TestMessage></ns1:TestMessage></ns1:SenderProvidedRequestData>

Поиск решения показал, что, во-первых форум КриптоПро, нормализованный документ может выглядеть на самом деле иначе и соответственно его хэш будет другой и возможно правильный.

Во-вторых, привел в GitHub, где был выложен класс SmevTransformSpi более старой версии.

Старая версия класса трансформации выдала следующий нормализованный документ:

<ns1:SenderProvidedRequestData xmlns:ns1="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/1.1" Id="SIGNED_BY_CONSUMER"><ns1:MessageID>db0486d0-3c08-11e5-95e2-d4c9eff07b77</ns1:MessageID><ns2:MessagePrimaryContent xmlns:ns2="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/basic/1.1"><ns3:BreachRequest xmlns:ns3="urn://x-artefacts-gibdd-gov-ru/breach/root/1.0" Id="PERSONAL_SIGNATURE"><ns3:RequestedInformation><ns4:RegPointNum xmlns:ns4="urn://x-artefacts-gibdd-gov-ru/breach/commons/1.0">Т785ЕС57</ns4:RegPointNum></ns3:RequestedInformation><ns3:Governance><ns5:Name xmlns:ns5="urn://x-artefacts-gibdd-gov-ru/breach/commons/1.0">ГИБДД РФ</ns5:Name><ns6:Code xmlns:ns6="urn://x-artefacts-gibdd-gov-ru/breach/commons/1.0">GIBDD</ns6:Code><ns7:OfficialPerson xmlns:ns7="urn://x-artefacts-gibdd-gov-ru/breach/commons/1.0"><ns8:FamilyName xmlns:ns8="urn://x-artefacts-smev-gov-ru/supplementary/commons/1.0.1">Загурский</ns8:FamilyName><ns9:FirstName xmlns:ns9="urn://x-artefacts-smev-gov-ru/supplementary/commons/1.0.1">Андрей</ns9:FirstName><ns10:Patronymic xmlns:ns10="urn://x-artefacts-smev-gov-ru/supplementary/commons/1.0.1">Петрович</ns10:Patronymic></ns7:OfficialPerson></ns3:Governance></ns3:BreachRequest></ns2:MessagePrimaryContent><ns1:TestMessage></ns1:TestMessage></ns1:SenderProvidedRequestData>

С ним хэш стал совпадать, а подпись успешно проходить валидацию.

Сравнение версий класса SmevTransformSpi показала, что помимо добавленных в новой реализации дополнительных функций логирования и диагностики в debug режиме:

if (logger.isDebugEnabled()) {
                debugStream = new DebugOutputStream(argDst);
                dst = outputFactory.get().createXMLEventWriter(debugStream, ENCODING_UTF_8);
            } else {
                dst = outputFactory.get().createXMLEventWriter(argDst, ENCODING_UTF_8);
            }

Класс из Методических указаний не содержит нужную строчку, или содержит опечатку:



Отсутствует строка:

prefixMappingStack.pop();

, которая удаляет первый объект из стека с префиксами

  Stack<List<Namespace>> prefixMappingStack = new Stack<List<Namespace>>();

, что приводило к неверной работе SmevTransformSpi.

Добавление этой строки в новую версию SmevTransformSpi.java решило проблему.

Работающий класс трансформации и конверт с подписью можно посмотреть в github.com/VBurmistrov/Smev3

Результаты


Подписание конвертов СМЭВ 3 выполняется успешно.

Сообщения проходят проверку на портале Электронного правительства Госуслуги



И в собственном приложении:

Альфа-Банк
Company

Comments 15

    +1
    Почему с криптопро и смэвом всегда всё не слава богу? Идут годы, меняются детали, не меняется главное — люди не прекращают пердолиться с этими вопросами снова и снова

    Два года прошло вот с этой статьи: habrahabr.ru/post/282225

    С тех пор у меня в личке отписались десятки людей, которые вообще не могут понять, что происходит и как подступиться к задаче. Учитывая что о своих проблемах пишут только самые активные — в реальности таких людей должно быть, сотни

    Имхо, правильный способ — сделать такое SDK, которое бы своим API показывало, как решать типичные вопросы. У людей не должно возникать вообще никаких вопросов по поводу самого типового сценария использования, это должен быть 1 метод апи, который работает железно.
      +1
      Здравствуйте, Олег!

      Я читал Вашу статью про Побег из КриптоПро. Такое нельзя пропускать.
      Безусловно мощно и полезно. Спасибо Вам!

      Но есть одна загвоздка, когда дело касается промышленного общения через СМЭВ, то речь идет о квалифицированной ЭП, а значит, как одно из требований, выработанной на СКЗИ от разработчика лицензиата ФСБ. И тут не убежишь, предусмотрена ответственность.

      В остальном для тестов и кейсов, что называется, для себя, то тут конечно можно достигнуть существенной экономии на лицензиях.

      По поводу сложностей в использовании:
      правильный способ — сделать такое SDK, которое бы своим API показывало, как решать типичные вопросы.


      Это было бы идеально.

      Но тут все усложняется тем, что лицензированный разработчик СКЗИ, например, КриптоПро — это одна организация, а ФГУП НИИ «Восход», на мощностях, которого подняты Головной удостоверяющий центр Минкомсвязь и СМЭВ — другая.

      У каждой есть своя документация, SDK и API, примеры, форум, портал технической поддержки и ситуационный центр.
      Причем есть конкретные примеры и инструкции, которые пересекаются, как например у КриптоПро для СМЭВ.

      Информации, конечно, много, разобраться сложно, но можно.

      Мне сразу вспоминается статья Каково оно учить JavaScript в 2016. Вот где зоопарк :-)

      А вообще для разработчиков Open Source разбираться в чем-то таком должно доставлять какое-то свое особое удовольствие :-)
        +1
        > И тут не убежишь, предусмотрена ответственность.

        Конечно же убежишь, у меня получилось. Рассказываю как: 1) Увольняешься из компании, которая занимается этой лабудой 2) PROFIT

        > А вообще для разработчиков Open Source

        Для разработчиков Free Software — может быть да. Но государственные заказы — это не Free, и даже не Open, а иногда ещё и серкетка

        > Информации, конечно, много, разобраться сложно, но можно.

        Разобраться в Apache Kafka можно минут за 30. То есть, от того момента как ты на Википедии прочитал, что такое Kafka, до того как у тебя начнут ходить сообщения, может пройти всего минут 30.

        Этого не происходит со СМЭВ, и у меня есть альтернативные объяснения, что происходит.

        — СМЭВ, его документация, и его SDK сделаны отвратительно

        — Крипто ПРО (по крайней мере Java-версия) сделаны отвратительно

        — Почти все работы по координации и администрированию этих (и других связанных) проектов сделаны отвратительно

        Этому уже никак не поможшь. Весь этот код можно только облить напалмом, сжечь, останки поместить в свинцовый гроб, замуровать в бетонный саркофаг с толщиной стенки сто метров, закопать на глубине 3 километров, и сверху насадить еловый лес, чтобы никто никогда не догадался, что ЭТО когда-то существовало.
          +1
          Да. Под Open Source я имел в виду в широком понимании, например, Open-Source JDK — Oracle в части публичных классов и предоставляемых APIs и различные компоненты и библиотеки с лицензией свободного программного обеспечения.

          Apache Kafka это конкретный инструмент со своим набором функций, tutorials и manuals, для которого тщательно обстругиваются сообществом.

          С RabbitMQ у меня тоже сообщения за день начали ходить.

          Другое дело, когда речь идет о целой распределенной инфраструктуре – Public Key Infrastructure.

          Куда можно отнести СКЗИ и СМЭВ, как разные ее составные инструменты, компоненты, системы.

          Инфраструктура с открытым ключом вбирает в себя технологии и знания из совершенно разных областей, с которыми требуется разбираться.

          Начиная с наборов различных стандартов X. группы PKIX, стандартов криптографии PKCS#, способов описания структур данных на ASN.1 с древовидной идентификацией OID и наборов криптографических алгоритмов до принципов работы Центров регистрации и сертификации, протоколов взаимодействия, стандартов ЭП. И заканчивая Java Cryptography Architecture (если используем Java) и знаниями в области XML (для XMLDSig и СМЭВ) и инструментов для работы с ним применительно к платформе.

          Дикая смесь и много чего еще, к которой добавляются описания и интерфейсы конкретных реализаций СКЗИ, СМЭВ, программно-аппаратных комплексов для УЦ и защиты и хранения ключей.

          Поэтому временные затраты на то, чтобы разобраться не сопоставимы.
            0
            Разработчику, которому нужно послать сообщение по СМЭВу, для выполнения этой задачи совершенно не нужна никакая криптография. Ему нужна функция sendMessage(что, куда).

            Всё остальное можно скрыть под капотом, и эта подкапотная часть будет подходить почти для всех аналогичных проектов. Обычно все именно так и делают.

            Ибо никому обычно не нужна вся эта муть с разными реализациями криптографии, от неё можно отбрехаться на уровне ТЗ.

            Трюк с запугиванием умными словами у тебя не получился :-)

            Никакого Open Source сообщества не нужно, чтобы догадаться о наличии метода sendMessage(что, куда) — для этого нужен всего лишь 1 программист, который хоть раз попробовал воспользоваться шнягой, которую он написал.

            То, что этот вопрос не проработан, навевает самые печальные мысли. Например о том, что писалось всё это силами десятка год-назад-студентов за еду.
              0
              Просто для интересу прошёл на портал (https://smev3.gosuslugi.ru/portal/)

              Увидел следующее:

              * Рекомендуемая версия библиотек для сборки клиента СМЭВ 3. Схема версии 1.1.
              * Рекомендуемая версия библиотек для сборки клиента СМЭВ 3. Схема версии 1.2.

              Самые важные ссылки расположены в самой жопе сайта, а сверху идёт всякий мусор (как и было всю дорогу, впрочем). Чудно.

              Кстати о мусоре. Методические рекомендации, не в маркдауне в гите, а в вордовских файлах. Ченжлог в начале документа. Прямо как в 2001 году оказался, верхом на машине времени. Внутри всё тот же позор, что и раньше. Судя по ченжлогу, несколько допиленный.

              Вернемся к «библиотекам». Открываю ссылку — там какой-то зип-архив. В корне всё так же нет LICENSE.txt.

              Ни тебе публичного гита, чтобы посмотреть конкретные коммиты. Ни лицензии. Ни ченжлога. Ни-че-го.

              Прошли годы, ага.

              Необучаемые.

              Вроде ко мне это уже и не относится никак, а от ярости руки трясутся, когда на всё это смотришь.

              Там в документах имена каких-то «инженеров» написаны. Инженеры они, значит. Вот бы им выдать люлей размером с дом.
                +1
                olegchir, а как же путь Бусидо́ 武士道

                Путь воина, который без ропота вступит в противоборство с самым отвратительным API или его отсутствием и разберется со всеми сложностями?! :-)

                Полезность здравой критики оспаривать не стану. :-)
                  0
                  ПОЛНОСТЬЮ ПОДДЕРЖИВАЮ!

                  Ядро линукс пересобрать легче чем установить смэв. Я до этого никогда не работал в гос секторе. Но тут пришлось. И в этой статье пишут как у них все круто сделано? это отвратительнейшая из программ когда либо созданный. что криптопро что смэв.

                  делаю докер контейнер для этого всего. и обертку к нему на python. чтобы у пользователя было 1 функция sent_message.

                  у меня впервые возникло желание убить человека. когда я начал пробывать пользоваться этим говном. я потратил на него неделю. НЕДЕЛЮ, КАРЛ!
          0
          С ним хэш стал совпадать, а подпись успешно проходить валидацию.

          Вот это кстати, полная засада.

          Реализация трансформатора не должна меняться.

          Каждая релизная версия трансформатора должна иметь уникальное имя.

          Если эти два требования не соблюдаются — то можно ЭЦП не использовать, он не выполняет своей функции.
            +1
            ggo,
            реализованная в SmevTransformSpi трансформация нормализует по заданным правилам XML документ перед тем, как будет посчитан его хэш и поставлена электронная подпись.

            Так что трансформация может быть какой угодно и на основные функции ЭП — защита от подмены информации и однозначное установление ее автора, трансформация не влияет.
              0
              Насколько я понимаю, перед тем как xml будет подписан к нему применяются разные трансформации, и уже результат подается на вход подписания.
              Если одна и та же трансформация, но из разных версий, дает разные результаты, то встает вопрос, как третья сторона будет доказывать или опровергать корректность подписи? Экспериментировать с доступными версиями трансформатора?
                0
                Класс SmevTransformSpi может дорабатываться, но набор правил трансформации, реализованных в нем, для конкретной версии схемы СМЭВ остается неизменным.
            +2
            Уважаемые коллеги,
            Всем привет!

            Пара важных дополнений:
            1) Я привел пример, как сначала подписать SOAP запрос, который можно еще корректировать в его части не подлежащей подписанию, а затем отправить.

            Мне поступил вопрос, как на лету сделать бесшовный вызов веб сервиса СМЭВ с подписанным запросом и так же проверить ответ.
            Хочу посоветовать мощную статью Aleksey Sushko на Old council
            Apache CXF и ЭЦП для SOAP сообщений СМЭВ

            В этой статье про квинтэссенцию использования технологии расширения SOAP запросов — WS-Security, Apache WSS4J и реализации JAX-WS — Apache CXF и все это на ГОСТ алгоритмах.
            Мне в свое время очень помогло, за что еще раз спасибо Алексею.

            2) Я сообщил в ФГУП НИИ «Восход» об опечатке в документе Методические рекомендации по работе с ЕСМЭВ версия 3.4.0.3.
              0
              Они к третьей версии выровняли метаданные по названиям атрибутов, типов данных и т.п. между разными участниками?
              А то помню приходилось держать по 7 версий для ФЛ и ЮЛ в 2012 году.

            Only users with full accounts can post comments. Log in, please.