libeufin

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

commit 264d6c496a6df8b32a5c7aca7b9ecce1947d3998
parent a9c4bb1841fa79220799f35feb587aaa8b247f2d
Author: Florian Dold <florian.dold@gmail.com>
Date:   Mon, 28 Oct 2019 18:37:39 +0100

signing / verification WIP

Diffstat:
Msandbox/build.gradle | 2++
Msandbox/src/main/kotlin/Main.kt | 16++++++++--------
Dsandbox/src/main/kotlin/XML.kt | 362-------------------------------------------------------------------------------
Asandbox/src/main/kotlin/XMLUtil.kt | 382+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msandbox/src/test/kotlin/HiaLoadTest.kt | 4++--
Msandbox/src/test/kotlin/JaxbTest.kt | 4++--
Msandbox/src/test/kotlin/MarshalNonJaxbTest.kt | 2+-
Msandbox/src/test/kotlin/ResponseTest.kt | 2+-
Msandbox/src/test/kotlin/XmlSigTest.kt | 24++++++++----------------
Msandbox/src/test/kotlin/XmlTest.kt | 5+----
Msandbox/src/test/kotlin/XsiTypeAttributeTest.kt | 6++----
11 files changed, 409 insertions(+), 400 deletions(-)

diff --git a/sandbox/build.gradle b/sandbox/build.gradle @@ -33,6 +33,8 @@ dependencies { compile "javax.activation:activation:1.1" compile "org.glassfish.jaxb:jaxb-runtime:2.3.1" testCompile group: 'junit', name: 'junit', version: '4.12' + testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.3.50' + testImplementation 'org.jetbrains.kotlin:kotlin-test:1.3.50' compile 'org.apache.santuario:xmlsec:2.1.4' runtime rootProject.files("resources") } diff --git a/sandbox/src/main/kotlin/Main.kt b/sandbox/src/main/kotlin/Main.kt @@ -36,6 +36,7 @@ import io.ktor.routing.post import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import io.ktor.util.pipeline.PipelineContext import org.jetbrains.exposed.dao.EntityID import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.LoggerFactory @@ -48,17 +49,15 @@ import java.math.BigInteger import java.nio.charset.StandardCharsets.US_ASCII import java.text.DateFormat import java.security.KeyFactory -import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey -import java.security.interfaces.RSAPrivateKey import java.security.spec.RSAPrivateKeySpec import java.security.spec.RSAPublicKeySpec import java.util.* import java.util.zip.InflaterInputStream val logger = LoggerFactory.getLogger("tech.libeufin.sandbox") -val xmlProcess = XML() +val xmlProcess = XMLUtil() val getEbicsHostId = {"LIBEUFIN-SANDBOX"} val getEbicsVersion = {"H004"} val getEbicsRevision = {1} @@ -149,7 +148,7 @@ object OkHelper { * @return the modified document */ fun downcastXml(document: Document, node: String, type: String) : Document { - logger.debug("Downcasting: ${XML.convertDomToString(document)}") + logger.debug("Downcasting: ${XMLUtil.convertDomToString(document)}") val x: Element = document.getElementsByTagNameNS( "urn:org:ebics:H004", "OrderDetails" @@ -234,7 +233,7 @@ private suspend fun ApplicationCall.adminCustomers() { logger.info(body.toString()) val returnId = transaction { - var myUserId = EbicsUser.new { } + val myUserId = EbicsUser.new { } val myPartnerId = EbicsPartner.new { } val mySystemId = EbicsSystem.new { } val subscriber = EbicsSubscriber.new { @@ -395,10 +394,10 @@ private suspend fun ApplicationCall.ebicsweb() { val body: String = receiveText() logger.debug("Data received: $body") - val bodyDocument: Document? = XML.parseStringIntoDom(body) + val bodyDocument: Document? = XMLUtil.parseStringIntoDom(body) if (bodyDocument == null || (!xmlProcess.validateFromDom(bodyDocument))) { - var response = EbicsResponse( + val response = EbicsResponse( returnCode = InvalidXmlHelper.getCode(), reportText = InvalidXmlHelper.getMessage() ) @@ -493,7 +492,7 @@ private suspend fun ApplicationCall.ebicsweb() { */ if (zkey.isEmpty()) { logger.info("0-length key element given, invalid request") - var response = KeyManagementResponse( + val response = KeyManagementResponse( returnCode = InvalidXmlHelper.getCode(), reportText = InvalidXmlHelper.getMessage("Key field was empty") ) @@ -678,6 +677,7 @@ private suspend fun ApplicationCall.ebicsweb() { } } + fun main() { dbCreateTables() val server = embeddedServer(Netty, port = 5000) { diff --git a/sandbox/src/main/kotlin/XML.kt b/sandbox/src/main/kotlin/XML.kt @@ -1,361 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -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 -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.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. - */ - private val validator = try { - val classLoader = ClassLoader.getSystemClassLoader() - val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") - sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) - sf.errorHandler = object : ErrorHandler { - override fun warning(p0: SAXParseException?) { - println("Warning: $p0") - } - - override fun error(p0: SAXParseException?) { - println("Error: $p0") - } - - override fun fatalError(p0: SAXParseException?) { - println("Fatal error: $p0") - } - } - sf.resourceResolver = object : LSResourceResolver { - override fun resolveResource( - type: String?, - namespaceURI: String?, - publicId: String?, - systemId: String?, - baseUri: String? - ): LSInput? { - if (type != "http://www.w3.org/2001/XMLSchema") { - return null - } - val res = classLoader.getResourceAsStream(systemId) ?: return null - return DOMInputImpl(publicId, systemId, baseUri, res, "UTF-8") - } - } - val schemaInputs: Array<Source> = listOf("ebics_H004.xsd", "ebics_hev.xsd").map { - val resUrl = classLoader.getResource(it) ?: throw FileNotFoundException("Schema file $it not found.") - StreamSource(File(resUrl.toURI())) - }.toTypedArray() - val bundle = sf.newSchema(schemaInputs) - bundle.newValidator() - } catch (e: SAXException) { - e.printStackTrace() - throw e - } - - - /** - * - * @param xmlDoc the XML document to validate - * @return true when validation passes, false otherwise - */ - fun validate(xmlDoc: StreamSource): Boolean { - try { - validator?.validate(xmlDoc) - } catch (e: SAXException) { - println(e.message) - return false - } catch (e: IOException) { - e.printStackTrace() - return false - } - - return true - } - - /** - * Validates the DOM against the Schema(s) of this object. - * @param domDocument DOM to validate - * @return true/false if the document is valid/invalid - */ - fun validateFromDom(domDocument: Document): Boolean { - try { - validator?.validate(DOMSource(domDocument)) - } catch (e: SAXException) { - e.printStackTrace() - return false - } - return true - } - - /** - * Craft object to be passed to the XML validator. - * @param xmlString XML body, as read from the POST body. - * @return InputStream object, as wanted by the validator. - */ - fun validateFromString(xmlString: String): Boolean { - val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) - val xmlSource = StreamSource(xmlInputStream) - return this.validate(xmlSource) - } - - /** - * Convert a DOM document - of a XML document - to the JAXB representation. - * - * @param finalType class type of the output - * @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> { - - val jc = JAXBContext.newInstance(finalType) - - /* Marshalling the object into the document. */ - val m = jc.createUnmarshaller() - return m.unmarshal(document, finalType) // document "went" into Jaxb - } - - /** - * Convert a XML string to the JAXB representation. - * - * @param finalType class type of the object to instantiate - * @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> { - - val jc = JAXBContext.newInstance(finalType.packageName) - - /* Marshalling the object into the document. */ - val u = jc.createUnmarshaller() - return u.unmarshal( - StreamSource(StringReader(documentString)), - finalType - ) // document "went" into Jaxb - } - - - /** - * Return the DOM representation of the Java object, using the JAXB - * interface. FIXME: narrow input type to JAXB type! - * - * @param object to be transformed into DOM. Typically, the object - * has already got its setters called. - * @return the DOM Document, or null (if errors occur). - */ - fun <T> convertJaxbToDom(obj: JAXBElement<T>): Document? { - - try { - val jc = JAXBContext.newInstance(obj.declaredType) - - /* Make the target document. */ - val dbf = DocumentBuilderFactory.newInstance() - val db = dbf.newDocumentBuilder() - val document = db.newDocument() - - /* Marshalling the object into the document. */ - val m = jc.createMarshaller() - m.marshal(obj, document) // document absorbed the XML! - return document - - } catch (e: JAXBException) { - e.printStackTrace() - } catch (e: ParserConfigurationException) { - e.printStackTrace() - } - 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() - - 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, "yes") - - /* Make string writer. */ - val sw = StringWriter() - - /* Extract string. */ - t.transform(DOMSource(document), StreamResult(sw)) - return sw.toString() - } - - 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() - } - - /** - * 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)) - } - - - 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) - } - - fun verifyEbicsDocument(doc: Document, signingPub: PublicKey): Boolean { - return false - } - } -} -\ No newline at end of file diff --git a/sandbox/src/main/kotlin/XMLUtil.kt b/sandbox/src/main/kotlin/XMLUtil.kt @@ -0,0 +1,381 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +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 +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.dom.DOMValidateContext +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.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 + +/** + * Helpers for dealing with XML in EBICS. + */ +class XMLUtil { + /** + * This URI dereferencer allows handling the resource reference used for + * XML signatures in EBICS. + */ + 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. + */ + private val validator = try { + val classLoader = ClassLoader.getSystemClassLoader() + val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") + sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) + sf.errorHandler = object : ErrorHandler { + override fun warning(p0: SAXParseException?) { + println("Warning: $p0") + } + + override fun error(p0: SAXParseException?) { + println("Error: $p0") + } + + override fun fatalError(p0: SAXParseException?) { + println("Fatal error: $p0") + } + } + sf.resourceResolver = object : LSResourceResolver { + override fun resolveResource( + type: String?, + namespaceURI: String?, + publicId: String?, + systemId: String?, + baseUri: String? + ): LSInput? { + if (type != "http://www.w3.org/2001/XMLSchema") { + return null + } + val res = classLoader.getResourceAsStream(systemId) ?: return null + return DOMInputImpl(publicId, systemId, baseUri, res, "UTF-8") + } + } + val schemaInputs: Array<Source> = listOf("ebics_H004.xsd", "ebics_hev.xsd").map { + val resUrl = classLoader.getResource(it) ?: throw FileNotFoundException("Schema file $it not found.") + StreamSource(File(resUrl.toURI())) + }.toTypedArray() + val bundle = sf.newSchema(schemaInputs) + bundle.newValidator() + } catch (e: SAXException) { + e.printStackTrace() + throw e + } + + + /** + * + * @param xmlDoc the XML document to validate + * @return true when validation passes, false otherwise + */ + fun validate(xmlDoc: StreamSource): Boolean { + try { + validator?.validate(xmlDoc) + } catch (e: Exception) { + logger.warn("Validation failed: {}", e) + return false + } + return true; + } + + /** + * Validates the DOM against the Schema(s) of this object. + * @param domDocument DOM to validate + * @return true/false if the document is valid/invalid + */ + fun validateFromDom(domDocument: Document): Boolean { + try { + validator?.validate(DOMSource(domDocument)) + } catch (e: SAXException) { + e.printStackTrace() + return false + } + return true + } + + /** + * Craft object to be passed to the XML validator. + * @param xmlString XML body, as read from the POST body. + * @return InputStream object, as wanted by the validator. + */ + fun validateFromString(xmlString: String): Boolean { + val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) + val xmlSource = StreamSource(xmlInputStream) + return this.validate(xmlSource) + } + + /** + * Convert a DOM document - of a XML document - to the JAXB representation. + * + * @param finalType class type of the output + * @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> { + + val jc = JAXBContext.newInstance(finalType) + + /* Marshalling the object into the document. */ + val m = jc.createUnmarshaller() + return m.unmarshal(document, finalType) // document "went" into Jaxb + } + + /** + * Convert a XML string to the JAXB representation. + * + * @param finalType class type of the object to instantiate + * @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> { + + val jc = JAXBContext.newInstance(finalType.packageName) + + /* Marshalling the object into the document. */ + val u = jc.createUnmarshaller() + return u.unmarshal( + StreamSource(StringReader(documentString)), + finalType + ) // document "went" into Jaxb + } + + + /** + * Return the DOM representation of the Java object, using the JAXB + * interface. FIXME: narrow input type to JAXB type! + * + * @param object to be transformed into DOM. Typically, the object + * has already got its setters called. + * @return the DOM Document, or null (if errors occur). + */ + fun <T> convertJaxbToDom(obj: JAXBElement<T>): Document? { + + try { + val jc = JAXBContext.newInstance(obj.declaredType) + + /* Make the target document. */ + val dbf = DocumentBuilderFactory.newInstance() + val db = dbf.newDocumentBuilder() + val document = db.newDocument() + + /* Marshalling the object into the document. */ + val m = jc.createMarshaller() + m.marshal(obj, document) // document absorbed the XML! + return document + + } catch (e: JAXBException) { + e.printStackTrace() + } catch (e: ParserConfigurationException) { + e.printStackTrace() + } + 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() + + 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, "yes") + + /* Make string writer. */ + val sw = StringWriter() + + /* Extract string. */ + t.transform(DOMSource(document), StreamResult(sw)) + return sw.toString() + } + + 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() + } + + /** + * 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)) + } + + + /** + * Sign an EBICS document with the authentication and identity signature. + */ + 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) + } + + fun verifyEbicsDocument(doc: Document, signingPub: PublicKey): Boolean { + val xpath = XPathFactory.newInstance().newXPath() + val doc2: Document = doc.cloneNode(true) as Document + val authSigNode = xpath.compile("/*[1]/AuthSignature").evaluate(doc2, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature") + authSigNode.parentNode.insertBefore(sigEl, authSigNode) + while (authSigNode.hasChildNodes()) { + sigEl.appendChild(authSigNode.firstChild) + } + authSigNode.parentNode.removeChild(authSigNode) + val fac = XMLSignatureFactory.getInstance("DOM") + println(convertDomToString(doc2)) + val dvc = DOMValidateContext(signingPub, sigEl) + dvc.uriDereferencer = EbicsSigUriDereferencer() + val sig = fac.unmarshalXMLSignature(dvc) + // FIXME: check that parameters are okay! + return sig.validate(dvc) + } + } +} +\ No newline at end of file diff --git a/sandbox/src/test/kotlin/HiaLoadTest.kt b/sandbox/src/test/kotlin/HiaLoadTest.kt @@ -9,10 +9,10 @@ class HiaLoadTest { @Test fun hiaLoad() { - val processor = XML() + val processor = XMLUtil() val classLoader = ClassLoader.getSystemClassLoader() val hia = classLoader.getResource("HIA.xml") - val hiaDom = XML.parseStringIntoDom(hia.readText()) + val hiaDom = XMLUtil.parseStringIntoDom(hia.readText()) val x: Element = hiaDom.getElementsByTagNameNS( "urn:org:ebics:H004", "OrderDetails" diff --git a/sandbox/src/test/kotlin/JaxbTest.kt b/sandbox/src/test/kotlin/JaxbTest.kt @@ -7,7 +7,7 @@ import tech.libeufin.messages.ebics.keyrequest.SignaturePubKeyOrderDataType class JaxbTest { - val processor = XML() + val processor = XMLUtil() val classLoader = ClassLoader.getSystemClassLoader() val hevResponseJaxb = HEVResponse( "000000", @@ -81,7 +81,7 @@ class JaxbTest { @Test fun domToJaxb() { val ini = classLoader.getResource("ebics_ini_request_sample_patched.xml") - val iniDom = XML.parseStringIntoDom(ini.readText()) + val iniDom = XMLUtil.parseStringIntoDom(ini.readText()) processor.convertDomToJaxb<EbicsUnsecuredRequest>( EbicsUnsecuredRequest::class.java, iniDom diff --git a/sandbox/src/test/kotlin/MarshalNonJaxbTest.kt b/sandbox/src/test/kotlin/MarshalNonJaxbTest.kt @@ -34,7 +34,7 @@ class MarshalNonJaxbTest { "EBICS_NON_OK" ) - val proc = XML() + val proc = XMLUtil() println(proc.convertJaxbToString(obj.get())) } } \ No newline at end of file diff --git a/sandbox/src/test/kotlin/ResponseTest.kt b/sandbox/src/test/kotlin/ResponseTest.kt @@ -4,7 +4,7 @@ import org.junit.Test class ResponseTest { - val xmlprocess = XML() + val xmlprocess = XMLUtil() @Test fun loadResponse() { diff --git a/sandbox/src/test/kotlin/XmlSigTest.kt b/sandbox/src/test/kotlin/XmlSigTest.kt @@ -1,27 +1,16 @@ 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 +import kotlin.test.* + class XmlSigTest { @Test fun basicSigningTest() { - val doc = XML.parseStringIntoDom(""" + val doc = XMLUtil.parseStringIntoDom(""" <foo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <AuthSignature /> <bar authenticate='true'>bla</bar>Hello World @@ -35,7 +24,10 @@ class XmlSigTest { val kpg = KeyPairGenerator.getInstance("RSA") kpg.initialize(2048) val pair = kpg.genKeyPair() - XML.signEbicsDocument(doc, pair.private) - println(XML.convertDomToString(doc)) + val otherPair = kpg.genKeyPair() + XMLUtil.signEbicsDocument(doc, pair.private) + println(XMLUtil.convertDomToString(doc)) + assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) + assertFalse(XMLUtil.verifyEbicsDocument(doc, otherPair.public)) } } \ No newline at end of file diff --git a/sandbox/src/test/kotlin/XmlTest.kt b/sandbox/src/test/kotlin/XmlTest.kt @@ -1,15 +1,12 @@ package tech.libeufin.sandbox -import org.junit.Assert import org.junit.Test import org.junit.Assert.* -import java.io.File -import javax.xml.transform.Source import javax.xml.transform.stream.StreamSource class XmlTest { - val processor = tech.libeufin.sandbox.XML() + val processor = tech.libeufin.sandbox.XMLUtil() @Test fun hevValidation(){ diff --git a/sandbox/src/test/kotlin/XsiTypeAttributeTest.kt b/sandbox/src/test/kotlin/XsiTypeAttributeTest.kt @@ -3,18 +3,16 @@ package tech.libeufin.sandbox import org.junit.Test import org.w3c.dom.Element import tech.libeufin.messages.ebics.keyrequest.EbicsUnsecuredRequest -import tech.libeufin.messages.ebics.keyrequest.OrderDetailsType -import tech.libeufin.messages.ebics.keyrequest.UnsecuredReqOrderDetailsType class XsiTypeAttributeTest { @Test fun domToJaxb() { - val processor = XML() + val processor = XMLUtil() val classLoader = ClassLoader.getSystemClassLoader() val ini = classLoader.getResource("ebics_ini_request_sample.xml") - val iniDom = XML.parseStringIntoDom(ini.readText()) + val iniDom = XMLUtil.parseStringIntoDom(ini.readText()) val x: Element = iniDom.getElementsByTagName("OrderDetails")?.item(0) as Element x.setAttributeNS(