libeufin

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

commit 567bf82e855829652b2250f83acd1f8fcf82e37c
parent 2d87b329c99cb2b335df317fe00faab941055b1a
Author: Antoine A <>
Date:   Tue,  5 Nov 2024 15:34:32 +0100

nexus: XML signature check while parsing

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt | 6+++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt | 32+++++++++++++++-----------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt | 6+++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt | 6+++---
Mnexus/src/test/kotlin/XmlCombinatorsTest.kt | 30+++++++++++++++++++++++++++---
5 files changed, 51 insertions(+), 29 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt @@ -100,9 +100,7 @@ object XMLUtil { } } - /** - * Sign an EBICS document with the authentication and identity signature. - */ + /** Sign an EBICS document with the authentication and identity signature */ fun signEbicsDocument( doc: Document, signingPriv: PrivateKey @@ -138,10 +136,12 @@ object XMLUtil { authSigNode.removeChild(innerSig) } + /** Check an EBICS document signature */ fun verifyEbicsDocument( doc: Document, signingPub: PublicKey ): Boolean { + // TODO can we simplify this ? val doc2: Document = doc.cloneNode(true) as Document val authSigNode = XPathFactory.newInstance().newXPath() .evaluate("/*[1]/*[local-name()='AuthSignature']", doc2, XPathConstants.NODE) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt @@ -125,34 +125,32 @@ private class XmlDOMBuilder(private val doc: Document, private val schema: Strin class DestructionError(m: String) : Exception(m) -private fun Element.childrenByTag(tag: String): Sequence<Element> = sequence { +private fun Element.childrenByTag(tag: String, signed: Boolean): Sequence<Element> = sequence { for (i in 0..childNodes.length) { val el = childNodes.item(i) - if (el !is Element) { - continue + if (el is Element + && el.localName == tag + && (!signed || el.getAttribute("authenticate") == "true")) { + yield(el) } - if (el.localName != tag) { - continue - } - yield(el) } } class XmlDestructor internal constructor(private val el: Element) { - fun each(path: String, f: XmlDestructor.() -> Unit) { - el.childrenByTag(path).forEach { + fun each(path: String, signed: Boolean = false, f: XmlDestructor.() -> Unit) { + el.childrenByTag(path, signed).forEach { f(XmlDestructor(it)) } } - fun <T> map(path: String, f: XmlDestructor.() -> T): List<T> { - return el.childrenByTag(path).map { + fun <T> map(path: String, signed: Boolean = false, f: XmlDestructor.() -> T): List<T> { + return el.childrenByTag(path, signed).map { f(XmlDestructor(it)) }.toList() } - fun one(tag: String): XmlDestructor { - val children = el.childrenByTag(tag).iterator() + fun one(tag: String, signed: Boolean = false): XmlDestructor { + val children = el.childrenByTag(tag, signed).iterator() if (!children.hasNext()) { throw DestructionError("expected unique '${el.tagName}.$tag', got none") } @@ -162,8 +160,8 @@ class XmlDestructor internal constructor(private val el: Element) { } return XmlDestructor(child) } - fun opt(tag: String): XmlDestructor? { - val children = el.childrenByTag(tag).iterator() + fun opt(tag: String, signed: Boolean = false): XmlDestructor? { + val children = el.childrenByTag(tag, signed).iterator() if (!children.hasNext()) { return null } @@ -174,8 +172,8 @@ class XmlDestructor internal constructor(private val el: Element) { return XmlDestructor(child) } - fun <T> one(path: String, f: XmlDestructor.() -> T): T = f(one(path)) - fun <T> opt(path: String, f: XmlDestructor.() -> T): T? = opt(path)?.run(f) + fun <T> one(path: String, signed: Boolean = false, f: XmlDestructor.() -> T): T = f(one(path, signed)) + fun <T> opt(path: String, signed: Boolean = false, f: XmlDestructor.() -> T): T? = opt(path, signed)?.run(f) fun text(): String = el.textContent fun bool(): Boolean = el.textContent.toBoolean() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt @@ -299,7 +299,7 @@ class EbicsBTS( var segmentNumber: Int? = null var segment: ByteArray? = null var dataEncryptionInfo: DataEncryptionInfo? = null - one("header") { + one("header", signed = true) { one("static") { transactionID = opt("TransactionID")?.text() numSegments = opt("NumSegments")?.text()?.toInt() @@ -313,14 +313,14 @@ class EbicsBTS( one("body") { opt("DataTransfer") { segment = one("OrderData").text().decodeBase64() - dataEncryptionInfo = opt("DataEncryptionInfo") { + dataEncryptionInfo = opt("DataEncryptionInfo", signed = true) { DataEncryptionInfo( one("TransactionKey").text().decodeBase64(), one("EncryptionPubKeyDigest").text().decodeBase64() ) } } - bankCode = EbicsReturnCode.lookup(one("ReturnCode").text()) + bankCode = EbicsReturnCode.lookup(one("ReturnCode", signed = true).text()) } EbicsResponse( bankCode = bankCode, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt @@ -139,15 +139,15 @@ class EbicsKeyMng( lateinit var technicalCode: EbicsReturnCode lateinit var bankCode: EbicsReturnCode var payload: InputStream? = null - one("header") { + one("header", signed = true) { one("mutable") { technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text()) } } one("body") { - bankCode = EbicsReturnCode.lookup(one("ReturnCode").text()) + bankCode = EbicsReturnCode.lookup(one("ReturnCode", signed = true).text()) payload = opt("DataTransfer") { - val descriptionInfo = one("DataEncryptionInfo") { + val descriptionInfo = one("DataEncryptionInfo", signed = true) { DataEncryptionInfo( one("TransactionKey").text().decodeBase64(), one("EncryptionPubKeyDigest").text().decodeBase64() diff --git a/nexus/src/test/kotlin/XmlCombinatorsTest.kt b/nexus/src/test/kotlin/XmlCombinatorsTest.kt @@ -17,17 +17,18 @@ * <http://www.gnu.org/licenses/> */ +import org.w3c.dom.Document import org.junit.Test -import tech.libeufin.nexus.XMLUtil -import tech.libeufin.nexus.XmlBuilder +import tech.libeufin.nexus.* import kotlin.test.assertEquals class XmlCombinatorsTest { - fun testBuilder(expected: String, root: String, builder: XmlBuilder.() -> Unit) { + fun testBuilder(expected: String, root: String, builder: XmlBuilder.() -> Unit): Document { val toBytes = XmlBuilder.toBytes(root, builder) val toDom = XmlBuilder.toDom(root, null, builder) //assertEquals(expected, toString) TODO fix empty tag being closed only with toString assertEquals(expected, XMLUtil.convertDomToBytes(toDom).toString(Charsets.UTF_8)) + return toDom } @Test @@ -73,4 +74,27 @@ class XmlCombinatorsTest { el("one_more") } } + + @Test + fun signed() { + val trapped = XmlBuilder.toDom("document", "urn:org:ebics:test") { + el("order") { + text("not signed") + } + el("order") { + attr("authenticate", "true") + text("signed") + } + el("order") { + attr("authenticate", "false") + text("not signed 2") + } + } + XmlDestructor.fromDoc(trapped, "document") { + assertEquals(3, map("order") { text() }.size) + one("order", signed = true) { + assertEquals("signed", text()) + } + } + } }