summaryrefslogtreecommitdiff
path: root/ebics/src/main/kotlin/XMLUtil.kt
diff options
context:
space:
mode:
Diffstat (limited to 'ebics/src/main/kotlin/XMLUtil.kt')
-rw-r--r--ebics/src/main/kotlin/XMLUtil.kt570
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