/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Copyright (c) 2015, MPL CodeInside http://codeinside.ru */ package ru.codeinside.gws.signature.injector; import org.apache.commons.codec.binary.Base64; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import ru.codeinside.gws.api.ClientRequest; import ru.codeinside.gws.api.CryptoProvider; import ru.codeinside.gws.api.ServerResponse; import ru.codeinside.gws.api.Signature; import ru.codeinside.gws.api.WrappedAppData; import ru.codeinside.gws.api.XmlNormalizer; import ru.codeinside.gws.api.XmlSignatureInjector; import ru.codeinside.gws.api.XmlTypes; import javax.xml.bind.DatatypeConverter; import javax.xml.bind.JAXBElement; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.soap.SOAPElement; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPMessage; import javax.xml.soap.SOAPPart; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.StringReader; import java.io.StringWriter; import java.security.cert.CertificateEncodingException; import java.util.logging.Logger; public final class XmlSignatureInjectorImp implements XmlSignatureInjector { private static final String APP_DATA = "AppData"; private static final String ACTOR_SMEV = "http://smev.gosuslugi.ru/actors/smev"; private static final String WSSE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; private static final String WSS_X509V3 = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"; private static final String WSS_BASE64_BINARY = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"; private static final String SignatureSpecNS = "http://www.w3.org/2000/09/xmldsig#"; final Logger log = Logger.getLogger(XmlSignatureInjector.class.getName()); @Override public String injectSpToAppData(WrappedAppData wrappedAppData) { // TODO: переписать под новые требования Document document = parseData(wrappedAppData.getWrappedAppData(), getDocumentBuilder()); validateAppData(document); Element signatureValue = document.createElementNS(XMLDSign.XMLNS, "ds:SignatureValue"); signatureValue.setTextContent(Base64.encodeBase64String(wrappedAppData.getSignature().sign)); XMLDSign.KeyInfo keyInfo = new XMLDSign.KeyInfo(); keyInfo.x509Data.setCertificate(wrappedAppData.getSignature().certificate); QName qName = new QName(XMLDSign.XMLNS, "KeyInfo"); JAXBElement<XMLDSign.KeyInfo> root = new JAXBElement<XMLDSign.KeyInfo>(qName, XMLDSign.KeyInfo.class, keyInfo); Element keyInfoElement = new XmlTypes(XMLDSign.KeyInfo.class).toElement(root, true); Node importNode = document.importNode(keyInfoElement, true); Element signature = findElement(document.getDocumentElement(), compileXPath("//*[local-name()='Signature']")); signature.appendChild(signatureValue); signature.appendChild(importNode); return contentToString(document); } /** * Встроить ЭП-ОВ в заголовок SOAP сообщения * * @param message * @param signature * @return */ @Override public void injectOvToSoapHeader(SOAPMessage message, Signature signature) { final SOAPPart doc = message.getSOAPPart(); try { SOAPElement security = (SOAPElement) message.getSOAPHeader().getChildElements().next(); SOAPElement signatureElement = (SOAPElement) security.getChildElements().next(); buildBinarySecurityToken(doc, security, signatureElement, signature); NodeList list = signatureElement.getChildNodes(); Element keyInfo = (Element) list.item(list.getLength() - 1); addSignedValueElement(doc, signatureElement, keyInfo, signature); } catch (SOAPException e) { e.printStackTrace(); throw new IllegalStateException("Ошибка встраивания подписи СП-ОВ в SOAP-сообщеие", e); } catch (CertificateEncodingException e) { e.printStackTrace(); throw new IllegalStateException("Ошибка встраивания подписи СП-ОВ в SOAP-сообщеие", e); } } @Override public void prepareSoapMessage(SOAPMessage message, byte[] bodyHash) { final SOAPPart doc = message.getSOAPPart(); try { Signature signature = new Signature(null, null, null, bodyHash, true); final SOAPElement security = buildSecurityElement(message); QName wsuId = doc.getEnvelope().createQName("Id", "wsu"); final String bodyId = message.getSOAPBody().getAttributeValue(wsuId); final XMLDSign xmldSign = new XMLDSign(signature, bodyId); addSignatureElement(doc, security, xmldSign); } catch (SOAPException e) { e.printStackTrace(); throw new IllegalStateException("Ошибка формирования заголовка SOAP-сообщения", e); } } @Override public byte[] prepareAppData(ClientRequest clientRequest, boolean isSignatureLast, XmlNormalizer normalizer, CryptoProvider cryptoProvider) { if (!clientRequest.signRequired) { return null; } String wrappedAppData = "<AppData Id=\"AppData\">" + clientRequest.appData + "</AppData>"; ByteArrayOutputStream normalizedSignedInfo = new ByteArrayOutputStream(); clientRequest.appData = processAppData(wrappedAppData, isSignatureLast, clientRequest.signingXPath, normalizedSignedInfo, normalizer, cryptoProvider); return normalizedSignedInfo.toByteArray(); } @Override public byte[] prepareAppData(ServerResponse serverResponse, boolean isSignatureLast, XmlNormalizer normalizer, CryptoProvider cryptoProvider) { if (!serverResponse.signRequired) { return null; } String wrappedAppData = "<AppData Id=\"AppData\">" + serverResponse.appData + "</AppData>"; ByteArrayOutputStream normalizedSignedInfo = new ByteArrayOutputStream(); serverResponse.appData = processAppData( wrappedAppData, isSignatureLast, serverResponse.signingXPath, normalizedSignedInfo, normalizer, cryptoProvider); return normalizedSignedInfo.toByteArray(); } private String processAppData( String appData, boolean isSignatureLast, String signingXPath, OutputStream normalizedSignedInfo, XmlNormalizer normalizer, CryptoProvider cryptoProvider) { Document appDataDocument = parseData(appData, getDocumentBuilder()); Element documentElement = appDataDocument.getDocumentElement(); XPathExpression signingPath = compileXPath(signingXPath); Element signingElement = findElement(documentElement, signingPath); Attr idAttr = getIdAttr(signingElement); ByteArrayOutputStream elementBytes = new ByteArrayOutputStream(); normalizer.normalize(signingElement, elementBytes); byte[] digestValue = cryptoProvider.digest(new ByteArrayInputStream(elementBytes.toByteArray())); Signature signature = new Signature(null, null, null, digestValue, true); Element signatureElement = assembleSignature(signature, idAttr.getValue()); Node importNode = appDataDocument.importNode(signatureElement, true); if (isSignatureLast) { documentElement.appendChild(importNode); } else { documentElement.insertBefore(importNode, documentElement.getFirstChild()); } Element signedInfoElement = findElement(documentElement, compileXPath("//*[local-name()='SignedInfo']")); normalizer.normalize(signedInfoElement, normalizedSignedInfo); return contentToString(appDataDocument); } private Element findElement(Element element, XPathExpression expression) { try { return (Element) expression.evaluate(element, XPathConstants.NODE); } catch (XPathExpressionException e) { throw new IllegalStateException("Не удалось найти блок для подписи: " + e.getMessage()); } } private XPathExpression compileXPath(String xPath) { if (xPath == null || "".equals(xPath)) { xPath = "/AppData"; } try { return XPathFactory.newInstance().newXPath().compile(xPath); } catch (XPathExpressionException e) { throw new IllegalStateException("Не удалось скомпилировать XPath: " + e.getMessage()); } } private Attr getIdAttr(Element element) { Attr attrId = element.getAttributeNode("Id"); if (attrId == null || attrId.getValue().isEmpty()) { return insertIdAttribute(element); } return attrId; } private Attr insertIdAttribute(Element element) { Attr id = element.getOwnerDocument().createAttribute("Id"); id.setValue(element.getLocalName()); element.setAttributeNode(id); return id; } private void validateAppData(Document document) { String localName = document.getDocumentElement().getLocalName(); if (!APP_DATA.equals(localName)) { throw new IllegalStateException("Expected 'AppData' tag, but was: " + localName); } } private String contentToString(Document document) { StreamResult result = new StreamResult(new StringWriter()); try { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); transformer.transform(new DOMSource(document), result); return result.getWriter().toString(); } catch (TransformerConfigurationException e) { throw new RuntimeException(e); } catch (TransformerException e) { throw new RuntimeException(e); } } private void insertSignatureToAppData(Document document, Element signatureElement, boolean isAppDataSignatureBlockLast) { Node imported = document.importNode(signatureElement, true); Element documentElement = document.getDocumentElement(); if (isAppDataSignatureBlockLast) { documentElement.appendChild(imported); } else { documentElement.insertBefore(imported, documentElement.getFirstChild()); } } private Element assembleSignature(Signature signature, String id) { XMLDSign xmldSign = new XMLDSign(signature, id); xmldSign.setEnveloped(true); return XmlTypes.beanToElement(xmldSign, true); } private Document parseData(String source, DocumentBuilder documentBuilder) { InputSource is = new InputSource(new StringReader(source)); try { return documentBuilder.parse(is); } catch (SAXException e) { log.severe("Unable to parse appData to Document: " + e.getMessage()); throw new RuntimeException(e); } catch (IOException e) { log.severe("IOException when parsing appData: " + e.getMessage()); throw new RuntimeException(e); } } private DocumentBuilder getDocumentBuilder() { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setIgnoringElementContentWhitespace(true); documentBuilderFactory.setNamespaceAware(true); documentBuilderFactory.setCoalescing(true); try { return documentBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException e) { log.severe("Unable to get DocumentBuilder: " + e.getMessage()); throw new RuntimeException(e); } } private SOAPElement buildSecurityElement(SOAPMessage message) throws SOAPException { final SOAPHeader header = message.getSOAPHeader(); final QName actor = header.createQName("actor", header.getPrefix()); final SOAPElement security = header.addChildElement("Security", "wsse", WSSE); security.addAttribute(actor, ACTOR_SMEV); return security; } private void buildBinarySecurityToken(SOAPPart doc, SOAPElement security, Element signatureElement, Signature signature) throws SOAPException, CertificateEncodingException { Element binarySecurityToken = doc.createElementNS(WSSE, "BinarySecurityToken"); binarySecurityToken.setPrefix(security.getPrefix()); binarySecurityToken.setAttribute("EncodingType", WSS_BASE64_BINARY); binarySecurityToken.setAttribute("ValueType", WSS_X509V3); binarySecurityToken.setTextContent(DatatypeConverter.printBase64Binary(signature.certificate.getEncoded())); binarySecurityToken.setAttribute("wsu:Id", "CertId"); security.insertBefore(binarySecurityToken, signatureElement); } private void addSignatureElement(SOAPPart doc, SOAPElement security, XMLDSign xmldSign) { Element signatureElement = XmlTypes.beanToElement(xmldSign, true); Element element = (Element) doc.importNode(signatureElement, true); Element keyInfo = doc.createElementNS(SignatureSpecNS, "KeyInfo"); Element securityTokenReference = doc.createElementNS(WSSE, "SecurityTokenReference"); Element reference = doc.createElementNS(WSSE, "Reference"); reference.setAttribute("URI", "#CertId"); reference.setAttribute("ValueType", WSS_X509V3); securityTokenReference.appendChild(reference); keyInfo.appendChild(securityTokenReference); element.appendChild(keyInfo); security.appendChild(element); } private void addSignedValueElement(SOAPPart doc, SOAPElement signatureElement, Element keyInfo, Signature signature) throws SOAPException { Element signatureValue = doc.createElementNS(SignatureSpecNS, "SignatureValue"); signatureValue.setPrefix(signatureElement.getPrefix()); signatureValue.setTextContent(DatatypeConverter.printBase64Binary(signature.sign)); signatureElement.insertBefore(signatureValue, keyInfo); } }