
Система межведомственного электронного взаимодействия (СМЭВ) задумывалась как цифровая среда предоставления услуг и исполнения государственных и муниципальных функций в электронной форме.
В настоящее время СМЭВ продолжает расширять свои возможности и вовлекать все большее количество участников взаимодействия.
Что оказалось как нельзя кстати, в том числе для коммерческих организаций, в частности банков, которые все больше стремятся перевести свои услуги в цифру и сериализовать процессы.
В этой статье мы поговорим о том, как своими силами подписать запросы и проверить электронные подписи ответов СМЭВ версии 3.0, и о паре интересных нюансов, с которыми пришлось при этом столкнуться.
Здравствуйте!
Может возникнуть вопрос. Почему своими силами? Когда для СМЭВ 3 есть целый Технологический портал, где
- опубликована вся документация и методические указания,
- есть раздел с часто задаваемыми вопросами,
- можно скачать актуальную версию библиотек клиента СМЭВ 3,
- предоставлены примеры полных конвертов сообщений с подписями,
- можно даже проверить онлайн свое сообщение или из примера на соответствие схемам сервиса СМЭВ и на предмет валидности его электронной подписи
Все верно, портал, безусловно, крайне полезный, и всеми его подсказками и инструментами можно и нужно пользоваться, но вот код на Java напишем свой.
По той простой причине, что уже есть собственная информационная система, работающая с форматами электронной подписи XMLDSig, XAdES, в которой применяются библиотеки проекта Apache Santuario, реализующие основные стандарты безопасности для XML. А также библиотеки, входящие в состав КриптоПро JCSP, помимо работы с XML, обеспечивающие API криптографических функций СКЗИ КриптоПро CSP.
Написание собственных методов для работы с электронными подписями СМЭВ 3 в данном случае выглядит более целесообразно, нежели разворачивание полного клиента поставляемого:
ФГБУ НИИ «Восход» (до 21 марта 2016 года ФГУП НИИ «Восход») или интеграция, его отдельных классов и пакетов.
В то же время заглянуть в открытый код клиента всегда полезно, а его наличие само по себе говорит о зрелости системы и высоком уровне поддержки.
Анализ исходных данных
Загружаем с портала СМЭВ 3:
- актуальную версию документа Методические рекомендации по работе с ЕСМЭВ версия 3.4.0.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 выполняется успешно.
Сообщения проходят проверку на портале Электронного правительства Госуслуги

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