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

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

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

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

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

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

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

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

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

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

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


Загружаем с портала СМЭВ 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 выполняется успешно.

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



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

Комментарии (7)


  1. olegchir
    01.03.2018 09:38

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

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

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

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


    1. vburmistrov Автор
      01.03.2018 11:45

      Здравствуйте, Олег!

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

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

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

      По поводу сложностей в использовании:

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


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

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

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

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

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

      А вообще для разработчиков Open Source разбираться в чем-то таком должно доставлять какое-то свое особое удовольствие :-)


  1. ggo
    01.03.2018 10:27

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

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

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

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

    Если эти два требования не соблюдаются — то можно ЭЦП не использовать, он не выполняет своей функции.


    1. vburmistrov Автор
      01.03.2018 10:53

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

      Так что трансформация может быть какой угодно и на основные функции ЭП — защита от подмены информации и однозначное установление ее автора, трансформация не влияет.


      1. ggo
        01.03.2018 14:13

        Насколько я понимаю, перед тем как xml будет подписан к нему применяются разные трансформации, и уже результат подается на вход подписания.
        Если одна и та же трансформация, но из разных версий, дает разные результаты, то встает вопрос, как третья сторона будет доказывать или опровергать корректность подписи? Экспериментировать с доступными версиями трансформатора?


        1. vburmistrov Автор
          01.03.2018 14:23

          Класс SmevTransformSpi может дорабатываться, но набор правил трансформации, реализованных в нем, для конкретной версии схемы СМЭВ остается неизменным.


  1. vburmistrov Автор
    01.03.2018 10:33

    Уважаемые коллеги,
    Всем привет!

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

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

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

    2) Я сообщил в ФГУП НИИ «Восход» об опечатке в документе Методические рекомендации по работе с ЕСМЭВ версия 3.4.0.3.