diff options
Diffstat (limited to 'ebics/src/main/kotlin/XMLUtil.kt')
-rw-r--r-- | ebics/src/main/kotlin/XMLUtil.kt | 570 |
1 files changed, 135 insertions, 435 deletions
diff --git a/ebics/src/main/kotlin/XMLUtil.kt b/ebics/src/main/kotlin/XMLUtil.kt index 3af1cd8c..b602adc0 100644 --- a/ebics/src/main/kotlin/XMLUtil.kt +++ b/ebics/src/main/kotlin/XMLUtil.kt @@ -19,7 +19,6 @@ package tech.libeufin.ebics -import com.sun.xml.bind.marshaller.NamespacePrefixMapper import io.ktor.http.* import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -32,15 +31,11 @@ import org.xml.sax.ErrorHandler import org.xml.sax.InputSource import org.xml.sax.SAXException import org.xml.sax.SAXParseException -import tech.libeufin.ebics.ebics_h004.EbicsResponse import java.io.* import java.security.PrivateKey import java.security.PublicKey import java.security.interfaces.RSAPrivateCrtKey import javax.xml.XMLConstants -import javax.xml.bind.JAXBContext -import javax.xml.bind.JAXBElement -import javax.xml.bind.Marshaller import javax.xml.crypto.* import javax.xml.crypto.dom.DOMURIReference import javax.xml.crypto.dsig.* @@ -64,449 +59,154 @@ import javax.xml.xpath.XPathFactory private val logger: Logger = LoggerFactory.getLogger("libeufin-xml") -class DefaultNamespaces : NamespacePrefixMapper() { - override fun getPreferredPrefix(namespaceUri: String?, suggestion: String?, requirePrefix: Boolean): String? { - if (namespaceUri == "http://www.w3.org/2000/09/xmldsig#") return "ds" - if (namespaceUri == XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI) return "xsi" - return null - } -} - -class DOMInputImpl : LSInput { - var fPublicId: String? = null - var fSystemId: String? = null - var fBaseSystemId: String? = null - var fByteStream: InputStream? = null - var fCharStream: Reader? = null - var fData: String? = null - var fEncoding: String? = null - var fCertifiedText = false - - override fun getByteStream(): InputStream? { - return fByteStream - } - - override fun setByteStream(byteStream: InputStream) { - fByteStream = byteStream - } - - override fun getCharacterStream(): Reader? { - return fCharStream - } - - override fun setCharacterStream(characterStream: Reader) { - fCharStream = characterStream - } - - override fun getStringData(): String? { - return fData - } - - override fun setStringData(stringData: String) { - fData = stringData - } - - override fun getEncoding(): String? { - return fEncoding - } - - override fun setEncoding(encoding: String) { - fEncoding = encoding - } - - override fun getPublicId(): String? { - return fPublicId - } - - override fun setPublicId(publicId: String) { - fPublicId = publicId - } - - override fun getSystemId(): String? { - return fSystemId - } - - override fun setSystemId(systemId: String) { - fSystemId = systemId - } - - override fun getBaseURI(): String? { - return fBaseSystemId - } - - override fun setBaseURI(baseURI: String) { - fBaseSystemId = baseURI - } - - override fun getCertifiedText(): Boolean { - return fCertifiedText - } - - override fun setCertifiedText(certifiedText: Boolean) { - fCertifiedText = certifiedText +/** + * 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 { + if (myRef !is DOMURIReference) + throw Exception("invalid type") + if (myRef.uri != "#xpointer(//*[@authenticate='true'])") + throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'") + val xp: XPath = XPathFactory.newInstance().newXPath() + val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").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 = ArrayList<Node>() + for (i in 0 until nodeSet.length) { + val node = nodeSet.item(i) + nodeList.add(node) + } + return NodeSetData { nodeList.iterator() } } } - /** * Helpers for dealing with XML in EBICS. */ -class XMLUtil private constructor() { +object XMLUtil { + fun convertDomToBytes(document: Document): ByteArray { + val w = ByteArrayOutputStream() + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.STANDALONE, "yes") + transformer.transform(DOMSource(document), StreamResult(w)) + return w.toByteArray() + } + /** - * This URI dereferencer allows handling the resource reference used for - * XML signatures in EBICS. + * Convert a node to a string without the XML declaration or + * indentation. */ - private class EbicsSigUriDereferencer : URIDereferencer { - override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { - if (myRef !is DOMURIReference) - throw Exception("invalid type") - if (myRef.uri != "#xpointer(//*[@authenticate='true'])") - throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'") - val xp: XPath = XPathFactory.newInstance().newXPath() - val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").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 = ArrayList<Node>() - for (i in 0 until nodeSet.length) { - val node = nodeSet.item(i) - nodeList.add(node) - } - return NodeSetData { nodeList.iterator() } - } + fun convertNodeToString(node: Node): String { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes") + /* Make string writer. */ + val sw = StringWriter() + /* Extract string. */ + t.transform(DOMSource(node), StreamResult(sw)) + return sw.toString() } - companion object { - private var cachedEbicsValidator: Validator? = null - private fun getEbicsValidator(): Validator { - val currentValidator = cachedEbicsValidator - if (currentValidator != null) - return currentValidator - 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("xsd/$systemId") ?: return null - return DOMInputImpl().apply { - fPublicId = publicId - fSystemId = systemId - fBaseSystemId = baseUri - fByteStream = res - fEncoding = "UTF-8" - } - } - } - val schemaInputs: Array<Source> = listOf( - "xsd/ebics_H004.xsd", - "xsd/ebics_H005.xsd", - "xsd/ebics_hev.xsd", - "xsd/camt.052.001.02.xsd", - "xsd/camt.053.001.02.xsd", - "xsd/camt.054.001.02.xsd", - "xsd/pain.001.001.03.xsd", - // "xsd/pain.001.001.03.ch.02.xsd", // Swiss 2013 version. - "xsd/pain.001.001.09.ch.03.xsd" // Swiss 2019 version. - ).map { - val stream = - classLoader.getResourceAsStream(it) ?: throw FileNotFoundException("Schema file $it not found.") - StreamSource(stream) - }.toTypedArray() - val bundle = sf.newSchema(schemaInputs) - val newValidator = bundle.newValidator() - cachedEbicsValidator = newValidator - return newValidator + /** Parse [xml] into a XML DOM */ + fun parseIntoDom(xml: InputStream): Document { + val factory = DocumentBuilderFactory.newInstance().apply { + isNamespaceAware = true } - - /** - * - * @param xmlDoc the XML document to validate - * @return true when validation passes, false otherwise - */ - @Synchronized fun validate(xmlDoc: StreamSource): Boolean { - try { - getEbicsValidator().validate(xmlDoc) - } catch (e: Exception) { - /** - * Would be convenient to return also the error - * message to the caller, so that it can link it - * to a document ID in the logs. - */ - 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 - */ - @Synchronized fun validateFromDom(domDocument: Document): Boolean { - try { - getEbicsValidator().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 validateFromBytes(xml: ByteArray): Boolean { - return validate(StreamSource(xml.inputStream())) - } - - inline fun <reified T> convertJaxbToBytes( - obj: T, - withSchemaLocation: String? = null - ): ByteArray { - val w = ByteArrayOutputStream() - val jc = JAXBContext.newInstance(T::class.java) - val m = jc.createMarshaller() - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - if (withSchemaLocation != null) { - m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) - } - m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) - m.marshal(obj, w) - return w.toByteArray() - } - - inline fun <reified T> convertJaxbToDocument( - obj: T, - withSchemaLocation: String? = null - ): Document { - val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() - dbf.isNamespaceAware = true - val doc = dbf.newDocumentBuilder().newDocument() - val jc = JAXBContext.newInstance(T::class.java) - val m = jc.createMarshaller() - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - if (withSchemaLocation != null) { - m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) - } - m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) - m.marshal(obj, doc) - return doc - } - - /** - * Convert XML bytes to the JAXB representation. - * - * @param documentBytes the bytes to convert into JAXB. - * @return the JAXB object reflecting the original XML document. - */ - inline fun <reified T> convertToJaxb(documentBytes: InputStream): JAXBElement<T> { - val jc = JAXBContext.newInstance(T::class.java) - val u = jc.createUnmarshaller() - return u.unmarshal( /* Marshalling the object into the document. */ - StreamSource(documentBytes), - T::class.java - ) - } - - fun convertDomToBytes(document: Document): ByteArray { - val w = ByteArrayOutputStream() - val transformer = TransformerFactory.newInstance().newTransformer() - transformer.setOutputProperty(OutputKeys.STANDALONE, "yes") - transformer.transform(DOMSource(document), StreamResult(w)) - return w.toByteArray() - } - - /** - * Convert a node to a string without the XML declaration or - * indentation. - */ - fun convertNodeToString(node: Node): String { - /* Make Transformer. */ - val tf = TransformerFactory.newInstance() - val t = tf.newTransformer() - t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes") - /* Make string writer. */ - val sw = StringWriter() - /* Extract string. */ - t.transform(DOMSource(node), StreamResult(sw)) - return sw.toString() - } - - /** - * Convert a DOM document to the JAXB representation. - * - * @param document the document to convert into JAXB. - * @return the JAXB object reflecting the original XML document. - */ - inline fun <reified T> convertDomToJaxb(document: Document): JAXBElement<T> { - val jc = JAXBContext.newInstance(T::class.java) - /* Marshalling the object into the document. */ - val m = jc.createUnmarshaller() - return m.unmarshal(document, T::class.java) // document "went" into Jaxb - } - - /** Parse [xml] into a XML DOM */ - fun parseIntoDom(xml: InputStream): Document { - val factory = DocumentBuilderFactory.newInstance().apply { - isNamespaceAware = true - } - val builder = factory.newDocumentBuilder() - return xml.use { - builder.parse(InputSource(it)) - } - } - - fun signEbicsResponse(ebicsResponse: EbicsResponse, privateKey: RSAPrivateCrtKey): ByteArray { - val doc = convertJaxbToDocument(ebicsResponse) - signEbicsDocument(doc, privateKey) - val signedDoc = convertDomToBytes(doc) - // logger.debug("response: $signedDoc") - return signedDoc - } - - /** - * Sign an EBICS document with the authentication and identity signature. - */ - fun signEbicsDocument( - doc: Document, - signingPriv: PrivateKey, - withEbics3: Boolean = false - ) { - val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" - val authSigNode = XPathFactory.newInstance().newXPath() - .evaluate("/*[1]/$ns:AuthSignature", 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("http://www.w3.org/2001/04/xmldsig-more#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() - dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true) - sig.sign(dsc) - val innerSig = authSigNode.firstChild - while (innerSig.hasChildNodes()) { - authSigNode.appendChild(innerSig.firstChild) - } - authSigNode.removeChild(innerSig) - } - - fun verifyEbicsDocument( - doc: Document, - signingPub: PublicKey, - withEbics3: Boolean = false - ): Boolean { - val doc2: Document = doc.cloneNode(true) as Document - val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" - val authSigNode = XPathFactory.newInstance().newXPath() - .evaluate("/*[1]/$ns:AuthSignature", 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") - val dvc = DOMValidateContext(signingPub, sigEl) - dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true) - dvc.uriDereferencer = EbicsSigUriDereferencer() - val sig = fac.unmarshalXMLSignature(dvc) - // FIXME: check that parameters are okay! - val valResult = sig.validate(dvc) - sig.signedInfo.references[0].validate(dvc) - return valResult - } - - fun getNodeFromXpath(doc: Document, query: String): Node { - val xpath = XPathFactory.newInstance().newXPath() - val ret = xpath.evaluate(query, doc, XPathConstants.NODE) - ?: throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") - return ret as Node - } - - fun getStringFromXpath(doc: Document, query: String): String { - val xpath = XPathFactory.newInstance().newXPath() - val ret = xpath.evaluate(query, doc, XPathConstants.STRING) as String - if (ret.isEmpty()) { - throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") - } - return ret + val builder = factory.newDocumentBuilder() + return xml.use { + builder.parse(InputSource(it)) } } -} -fun Document.pickString(xpath: String): String { - return XMLUtil.getStringFromXpath(this, xpath) -} - -fun Document.pickStringWithRootNs(xpathQuery: String): String { - val doc = this - val xpath = XPathFactory.newInstance().newXPath() - xpath.namespaceContext = object : NamespaceContext { - override fun getNamespaceURI(p0: String?): String { - return when (p0) { - "root" -> doc.documentElement.namespaceURI - else -> throw IllegalArgumentException() - } - } - - override fun getPrefix(p0: String?): String { - throw UnsupportedOperationException() - } - - override fun getPrefixes(p0: String?): MutableIterator<String> { - throw UnsupportedOperationException() - } - } - val ret = xpath.evaluate(xpathQuery, this, XPathConstants.STRING) as String - if (ret.isEmpty()) { - throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $xpathQuery") + /** + * Sign an EBICS document with the authentication and identity signature. + */ + fun signEbicsDocument( + doc: Document, + signingPriv: PrivateKey, + withEbics3: Boolean = false + ) { + val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" + val authSigNode = XPathFactory.newInstance().newXPath() + .evaluate("/*[1]/$ns:AuthSignature", 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("http://www.w3.org/2001/04/xmldsig-more#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() + dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true) + sig.sign(dsc) + val innerSig = authSigNode.firstChild + while (innerSig.hasChildNodes()) { + authSigNode.appendChild(innerSig.firstChild) + } + authSigNode.removeChild(innerSig) + } + + fun verifyEbicsDocument( + doc: Document, + signingPub: PublicKey, + withEbics3: Boolean = false + ): Boolean { + val doc2: Document = doc.cloneNode(true) as Document + val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" + val authSigNode = XPathFactory.newInstance().newXPath() + .evaluate("/*[1]/$ns:AuthSignature", 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") + val dvc = DOMValidateContext(signingPub, sigEl) + dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true) + dvc.uriDereferencer = EbicsSigUriDereferencer() + val sig = fac.unmarshalXMLSignature(dvc) + // FIXME: check that parameters are okay! + val valResult = sig.validate(dvc) + sig.signedInfo.references[0].validate(dvc) + return valResult + } + + fun getNodeFromXpath(doc: Document, query: String): Node { + val xpath = XPathFactory.newInstance().newXPath() + val ret = xpath.evaluate(query, doc, XPathConstants.NODE) + ?: throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") + return ret as Node + } + + fun getStringFromXpath(doc: Document, query: String): String { + val xpath = XPathFactory.newInstance().newXPath() + val ret = xpath.evaluate(query, doc, XPathConstants.STRING) as String + if (ret.isEmpty()) { + throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") + } + return ret } - return ret }
\ No newline at end of file |