libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit a9c4bb1841fa79220799f35feb587aaa8b247f2d
parent 3a53d8825b47d71232d6d1a4e19164515be29020
Author: Florian Dold <florian.dold@gmail.com>
Date:   Mon, 28 Oct 2019 15:51:43 +0100

implement XML signing, minor refactoring

Diffstat:
A.idea/dictionaries/dold.xml | 8++++++++
M.idea/gradle.xml | 2+-
M.idea/misc.xml | 2+-
Msandbox/build.gradle | 1+
Msandbox/src/main/java/tech/libeufin/messages/ebics/hev/ObjectFactory.java | 2+-
Msandbox/src/main/kotlin/Main.kt | 4++--
Msandbox/src/main/kotlin/XML.kt | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msandbox/src/test/kotlin/GeneratePrivateKeyTest.kt | 2+-
Msandbox/src/test/kotlin/HiaLoadTest.kt | 13++++++-------
Msandbox/src/test/kotlin/JaxbTest.kt | 6+++---
Asandbox/src/test/kotlin/XmlSigTest.kt | 42++++++++++++++++++++++++++++++++++++++++++
Msandbox/src/test/kotlin/XsiTypeAttributeTest.kt | 4++--
12 files changed, 199 insertions(+), 80 deletions(-)

diff --git a/.idea/dictionaries/dold.xml b/.idea/dictionaries/dold.xml @@ -0,0 +1,7 @@ +<component name="ProjectDictionaryState"> + <dictionary name="dold"> + <words> + <w>ebics</w> + </words> + </dictionary> +</component> +\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml @@ -9,7 +9,7 @@ <option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="gradleHome" value="/usr/share/java/gradle" /> - <option name="gradleJvm" value="11" /> + <option name="gradleJvm" value="#JAVA_INTERNAL" /> <option name="modules"> <set> <option value="$PROJECT_DIR$" /> diff --git a/.idea/misc.xml b/.idea/misc.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> - <component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK" /> + <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="12" project-jdk-type="JavaSDK" /> </project> \ No newline at end of file diff --git a/sandbox/build.gradle b/sandbox/build.gradle @@ -33,6 +33,7 @@ dependencies { compile "javax.activation:activation:1.1" compile "org.glassfish.jaxb:jaxb-runtime:2.3.1" testCompile group: 'junit', name: 'junit', version: '4.12' + compile 'org.apache.santuario:xmlsec:2.1.4' runtime rootProject.files("resources") } diff --git a/sandbox/src/main/java/tech/libeufin/messages/ebics/hev/ObjectFactory.java b/sandbox/src/main/java/tech/libeufin/messages/ebics/hev/ObjectFactory.java @@ -11,7 +11,7 @@ import javax.xml.namespace.QName; * This object contains factory methods for each * Java content interface and Java element interface * generated in the tech.libeufin package. - * <p>An ObjectFactory allows you to programatically + * <p>An ObjectFactory allows you to programmatically * construct new instances of the Java representation * for XML content. The Java representation of XML * content can consist of schema derived interfaces diff --git a/sandbox/src/main/kotlin/Main.kt b/sandbox/src/main/kotlin/Main.kt @@ -149,7 +149,7 @@ object OkHelper { * @return the modified document */ fun downcastXml(document: Document, node: String, type: String) : Document { - logger.debug("Downcasting: ${xmlProcess.convertDomToString(document)}") + logger.debug("Downcasting: ${XML.convertDomToString(document)}") val x: Element = document.getElementsByTagNameNS( "urn:org:ebics:H004", "OrderDetails" @@ -395,7 +395,7 @@ private suspend fun ApplicationCall.ebicsweb() { val body: String = receiveText() logger.debug("Data received: $body") - val bodyDocument: Document? = xmlProcess.parseStringIntoDom(body) + val bodyDocument: Document? = XML.parseStringIntoDom(body) if (bodyDocument == null || (!xmlProcess.validateFromDom(bodyDocument))) { var response = EbicsResponse( diff --git a/sandbox/src/main/kotlin/XML.kt b/sandbox/src/main/kotlin/XML.kt @@ -21,6 +21,8 @@ package tech.libeufin.sandbox import com.sun.org.apache.xerces.internal.dom.DOMInputImpl import org.w3c.dom.Document +import org.w3c.dom.Node +import org.w3c.dom.NodeList import org.w3c.dom.ls.LSInput import org.w3c.dom.ls.LSResourceResolver import org.xml.sax.ErrorHandler @@ -28,25 +30,60 @@ import org.xml.sax.InputSource import org.xml.sax.SAXException import org.xml.sax.SAXParseException import java.io.* +import java.security.PrivateKey +import java.security.PublicKey +import java.util.* import javax.xml.XMLConstants import javax.xml.bind.JAXBContext import javax.xml.bind.JAXBElement import javax.xml.bind.JAXBException import javax.xml.bind.Marshaller +import javax.xml.crypto.* +import javax.xml.crypto.dom.DOMURIReference +import javax.xml.crypto.dsig.* +import javax.xml.crypto.dsig.dom.DOMSignContext +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec +import javax.xml.crypto.dsig.spec.TransformParameterSpec import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.ParserConfigurationException -import javax.xml.transform.* +import javax.xml.transform.OutputKeys +import javax.xml.transform.Source +import javax.xml.transform.TransformerFactory import javax.xml.transform.dom.DOMSource import javax.xml.transform.stream.StreamResult import javax.xml.transform.stream.StreamSource import javax.xml.validation.SchemaFactory - +import javax.xml.xpath.XPath +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory /** * This class takes care of importing XSDs and validate * XMLs against those. */ class XML { + private class EbicsSigUriDereferencer : URIDereferencer { + override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { + val ebicsXpathExpr = "//*[@authenticate='true']" + if (myRef !is DOMURIReference) + throw Exception("invalid type") + if (myRef.uri != "#xpointer($ebicsXpathExpr)") + throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'") + val xp: XPath = XPathFactory.newInstance().newXPath() + val nodeSet = xp.compile(ebicsXpathExpr).evaluate(myRef.here.ownerDocument, XPathConstants.NODESET) + if (nodeSet !is NodeList) + throw Exception("invalid type") + if (nodeSet.length <= 0) { + throw Exception("no nodes to sign") + } + val nodeList = LinkedList<Node>() + for (i in 0 until nodeSet.length) { + nodeList.add(nodeSet.item(i)) + } + return NodeSetData { nodeList.iterator() } + } + } + /** * Validator for EBICS messages. */ @@ -97,33 +134,6 @@ class XML { /** - * Parse string into XML DOM. - * @param xmlString the string to parse. - * @return the DOM representing @a xmlString - */ - fun parseStringIntoDom(xmlString: String): Document? { - - val factory = DocumentBuilderFactory.newInstance() - factory.isNamespaceAware = true - - try { - val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) - val builder = factory.newDocumentBuilder() - val document = builder.parse(InputSource(xmlInputStream)) - - return document - - } catch (e: ParserConfigurationException) { - e.printStackTrace() - } catch (e: SAXException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } - return null - } - - /** * * @param xmlDoc the XML document to validate * @return true when validation passes, false otherwise @@ -175,7 +185,7 @@ class XML { * @param document the document to convert into JAXB. * @return the JAXB object reflecting the original XML document. */ - fun <T>convertDomToJaxb(finalType: Class<T>, document: Document) : JAXBElement<T> { + fun <T> convertDomToJaxb(finalType: Class<T>, document: Document): JAXBElement<T> { val jc = JAXBContext.newInstance(finalType) @@ -191,7 +201,7 @@ class XML { * @param documentString the string to convert into JAXB. * @return the JAXB object reflecting the original XML document. */ - fun <T>convertStringToJaxb(finalType: Class<T>, documentString: String) : JAXBElement<T> { + fun <T> convertStringToJaxb(finalType: Class<T>, documentString: String): JAXBElement<T> { val jc = JAXBContext.newInstance(finalType.packageName) @@ -204,7 +214,6 @@ class XML { } - /** * Return the DOM representation of the Java object, using the JAXB * interface. FIXME: narrow input type to JAXB type! @@ -213,7 +222,7 @@ class XML { * has already got its setters called. * @return the DOM Document, or null (if errors occur). */ - fun <T>convertJaxbToDom(obj: JAXBElement<T>): Document? { + fun <T> convertJaxbToDom(obj: JAXBElement<T>): Document? { try { val jc = JAXBContext.newInstance(obj.declaredType) @@ -237,19 +246,42 @@ class XML { } /** - * Extract String from DOM. + * Extract String from JAXB. * - * @param document the DOM to extract the string from. - * @return the final String, or null if errors occur. + * @param obj the JAXB instance + * @return String representation of @a object, or null if errors occur */ - fun convertDomToString(document: Document): String? { + fun <T> convertJaxbToString(obj: JAXBElement<T>): String? { + val sw = StringWriter() try { + val jc = JAXBContext.newInstance(obj.declaredType) + /* Getting the string. */ + val m = jc.createMarshaller() + m.marshal(obj, sw) + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + + } catch (e: JAXBException) { + e.printStackTrace() + return "Bank fatal error." + } + + return sw.toString() + } + + companion object { + /** + * Extract String from DOM. + * + * @param document the DOM to extract the string from. + * @return the final String, or null if errors occur. + */ + fun convertDomToString(document: Document): String? { /* Make Transformer. */ val tf = TransformerFactory.newInstance() val t = tf.newTransformer() - t.setOutputProperty(OutputKeys.INDENT, "no") + t.setOutputProperty(OutputKeys.INDENT, "yes") /* Make string writer. */ val sw = StringWriter() @@ -257,36 +289,73 @@ class XML { /* Extract string. */ t.transform(DOMSource(document), StreamResult(sw)) return sw.toString() + } - } catch (e: TransformerConfigurationException) { - e.printStackTrace() - } catch (e: TransformerException) { - e.printStackTrace() + fun convertNodeToString(node: Node): String? { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + + t.setOutputProperty(OutputKeys.INDENT, "yes") + + /* Make string writer. */ + val sw = StringWriter() + + /* Extract string. */ + t.transform(DOMSource(node), StreamResult(sw)) + return sw.toString() } - return null - } - /** - * Extract String from JAXB. - * - * @param obj the JAXB instance - * @return String representation of @a object, or null if errors occur - */ - fun <T> convertJaxbToString(obj: JAXBElement<T>): String? { - val sw = StringWriter() + /** + * Parse string into XML DOM. + * @param xmlString the string to parse. + * @return the DOM representing @a xmlString + */ + fun parseStringIntoDom(xmlString: String): Document { + val factory = DocumentBuilderFactory.newInstance().apply { + isNamespaceAware = true + } + val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) + val builder = factory.newDocumentBuilder() + return builder.parse(InputSource(xmlInputStream)) + } - try { - val jc = JAXBContext.newInstance(obj.declaredType) - /* Getting the string. */ - val m = jc.createMarshaller() - m.marshal(obj, sw) - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - } catch (e: JAXBException) { - e.printStackTrace() - return "Bank fatal error." + fun signEbicsDocument(doc: Document, signingPriv: PrivateKey): Unit { + val xpath = XPathFactory.newInstance().newXPath() + val authSigNode = xpath.compile("/*[1]/AuthSignature").evaluate(doc, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val fac = XMLSignatureFactory.getInstance("DOM") + val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?) + val ref: Reference = + fac.newReference( + "#xpointer(//*[@authenticate='true'])", + fac.newDigestMethod(DigestMethod.SHA256, null), + listOf(c14n), + null, + null + ) + val canon: CanonicalizationMethod = + fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?) + val signatureMethod = fac.newSignatureMethod(SignatureMethod.RSA_SHA256, null) + val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref)) + val sig: XMLSignature = fac.newXMLSignature(si, null) + val dsc = DOMSignContext(signingPriv, authSigNode) + dsc.defaultNamespacePrefix = "ds" + dsc.uriDereferencer = EbicsSigUriDereferencer() + + sig.sign(dsc) + + val innerSig = authSigNode.firstChild + while (innerSig.hasChildNodes()) { + authSigNode.appendChild(innerSig.firstChild) + } + authSigNode.removeChild(innerSig) } - return sw.toString() + fun verifyEbicsDocument(doc: Document, signingPub: PublicKey): Boolean { + return false + } } } \ No newline at end of file diff --git a/sandbox/src/test/kotlin/GeneratePrivateKeyTest.kt b/sandbox/src/test/kotlin/GeneratePrivateKeyTest.kt @@ -15,7 +15,7 @@ class GeneratePrivateKeyTest { @Test fun loadOrGeneratePrivateKey() { - val x = getOrMakePrivateKey() + getOrMakePrivateKey() assertTrue( transaction { diff --git a/sandbox/src/test/kotlin/HiaLoadTest.kt b/sandbox/src/test/kotlin/HiaLoadTest.kt @@ -12,10 +12,11 @@ class HiaLoadTest { val processor = XML() val classLoader = ClassLoader.getSystemClassLoader() val hia = classLoader.getResource("HIA.xml") - val hiaDom = processor.parseStringIntoDom(hia.readText()) - val x: Element = hiaDom?.getElementsByTagNameNS( + val hiaDom = XML.parseStringIntoDom(hia.readText()) + val x: Element = hiaDom.getElementsByTagNameNS( "urn:org:ebics:H004", - "OrderDetails")?.item(0) as Element + "OrderDetails" + )?.item(0) as Element x.setAttributeNS( "http://www.w3.org/2001/XMLSchema-instance", @@ -25,9 +26,7 @@ class HiaLoadTest { processor.convertDomToJaxb<EbicsUnsecuredRequest>( EbicsUnsecuredRequest::class.java, - hiaDom!!) + hiaDom + ) } - - - } \ No newline at end of file diff --git a/sandbox/src/test/kotlin/JaxbTest.kt b/sandbox/src/test/kotlin/JaxbTest.kt @@ -80,11 +80,11 @@ class JaxbTest { */ @Test fun domToJaxb() { - val ini = classLoader.getResource("ebics_ini_request_sample_patched.xml") - val iniDom = processor.parseStringIntoDom(ini.readText()) + val iniDom = XML.parseStringIntoDom(ini.readText()) processor.convertDomToJaxb<EbicsUnsecuredRequest>( EbicsUnsecuredRequest::class.java, - iniDom!!) + iniDom + ) } } \ No newline at end of file diff --git a/sandbox/src/test/kotlin/XmlSigTest.kt b/sandbox/src/test/kotlin/XmlSigTest.kt @@ -0,0 +1,41 @@ +package tech.libeufin.sandbox + +import org.junit.Test +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import java.security.KeyPairGenerator +import java.util.* +import javax.xml.crypto.NodeSetData +import javax.xml.crypto.URIDereferencer +import javax.xml.crypto.dom.DOMURIReference +import javax.xml.crypto.dsig.* +import javax.xml.crypto.dsig.dom.DOMSignContext +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec +import javax.xml.crypto.dsig.spec.TransformParameterSpec +import javax.xml.xpath.XPath +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + + +class XmlSigTest { + + @Test + fun basicSigningTest() { + val doc = XML.parseStringIntoDom(""" + <foo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> + <AuthSignature /> + <bar authenticate='true'>bla</bar>Hello World + <spam> + eggs + + ham + </spam> + </foo> + """.trimIndent()) + val kpg = KeyPairGenerator.getInstance("RSA") + kpg.initialize(2048) + val pair = kpg.genKeyPair() + XML.signEbicsDocument(doc, pair.private) + println(XML.convertDomToString(doc)) + } +} +\ No newline at end of file diff --git a/sandbox/src/test/kotlin/XsiTypeAttributeTest.kt b/sandbox/src/test/kotlin/XsiTypeAttributeTest.kt @@ -14,8 +14,8 @@ class XsiTypeAttributeTest { val processor = XML() val classLoader = ClassLoader.getSystemClassLoader() val ini = classLoader.getResource("ebics_ini_request_sample.xml") - val iniDom = processor.parseStringIntoDom(ini.readText()) - val x: Element = iniDom?.getElementsByTagName("OrderDetails")?.item(0) as Element + val iniDom = XML.parseStringIntoDom(ini.readText()) + val x: Element = iniDom.getElementsByTagName("OrderDetails")?.item(0) as Element x.setAttributeNS( "http://www.w3.org/2001/XMLSchema-instance",