summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-03-07 00:49:21 +0100
committerAntoine A <>2024-03-07 00:49:21 +0100
commit2cd837fef9dc1516814307f49e1bee5a0a3bcec3 (patch)
treeaca9fdb4306b8b61ff9f3a0c523a4ccdc63b8828
parent509c14f45fcbeb1eb09da1e16fa0ba4958db6854 (diff)
downloadlibeufin-2cd837fef9dc1516814307f49e1bee5a0a3bcec3.tar.gz
libeufin-2cd837fef9dc1516814307f49e1bee5a0a3bcec3.tar.bz2
libeufin-2cd837fef9dc1516814307f49e1bee5a0a3bcec3.zip
Use XML builder for Ebics3 download initialization
-rw-r--r--ebics/src/main/kotlin/XMLUtil.kt56
-rw-r--r--ebics/src/main/kotlin/XmlCombinators.kt98
-rw-r--r--ebics/src/test/kotlin/XmlCombinatorsTest.kt37
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt2
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt119
5 files changed, 182 insertions, 130 deletions
diff --git a/ebics/src/main/kotlin/XMLUtil.kt b/ebics/src/main/kotlin/XMLUtil.kt
index fbebc124..3af1cd8c 100644
--- a/ebics/src/main/kotlin/XMLUtil.kt
+++ b/ebics/src/main/kotlin/XMLUtil.kt
@@ -158,10 +158,9 @@ class XMLUtil private constructor() {
*/
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)")
+ 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(
@@ -341,15 +340,10 @@ class XMLUtil private constructor() {
}
fun convertDomToBytes(document: Document): ByteArray {
- /* Make Transformer. */
- val tf = TransformerFactory.newInstance()
- val t = tf.newTransformer()
-
- /* Make bytes writer. */
val w = ByteArrayOutputStream()
-
- /* Extract string. */
- t.transform(DOMSource(document), StreamResult(w))
+ val transformer = TransformerFactory.newInstance().newTransformer()
+ transformer.setOutputProperty(OutputKeys.STANDALONE, "yes")
+ transformer.transform(DOMSource(document), StreamResult(w))
return w.toByteArray()
}
@@ -409,24 +403,9 @@ class XMLUtil private constructor() {
signingPriv: PrivateKey,
withEbics3: Boolean = false
) {
- val xpath = XPathFactory.newInstance().newXPath()
- xpath.namespaceContext = object : NamespaceContext {
- override fun getNamespaceURI(p0: String?): String {
- return when (p0) {
- "ebics" -> if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004"
- else -> throw IllegalArgumentException()
- }
- }
-
- override fun getPrefix(p0: String?): String {
- throw UnsupportedOperationException()
- }
-
- override fun getPrefixes(p0: String?): MutableIterator<String> {
- throw UnsupportedOperationException()
- }
- }
- val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc, XPathConstants.NODE)
+ 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")
@@ -461,25 +440,10 @@ class XMLUtil private constructor() {
signingPub: PublicKey,
withEbics3: Boolean = false
): Boolean {
- val xpath = XPathFactory.newInstance().newXPath()
- xpath.namespaceContext = object : NamespaceContext {
- override fun getNamespaceURI(p0: String?): String {
- return when (p0) {
- "ebics" -> if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004"
- else -> throw IllegalArgumentException()
- }
- }
-
- override fun getPrefix(p0: String?): String {
- throw UnsupportedOperationException()
- }
-
- override fun getPrefixes(p0: String?): MutableIterator<String> {
- throw UnsupportedOperationException()
- }
- }
val doc2: Document = doc.cloneNode(true) as Document
- val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc2, XPathConstants.NODE)
+ 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")
diff --git a/ebics/src/main/kotlin/XmlCombinators.kt b/ebics/src/main/kotlin/XmlCombinators.kt
index ce1c9f37..9cb0b327 100644
--- a/ebics/src/main/kotlin/XmlCombinators.kt
+++ b/ebics/src/main/kotlin/XmlCombinators.kt
@@ -19,17 +19,62 @@
package tech.libeufin.ebics
-import org.w3c.dom.Element
+import org.w3c.dom.*
import java.io.InputStream
import java.io.StringWriter
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
+import javax.xml.parsers.*
import javax.xml.stream.XMLOutputFactory
import javax.xml.stream.XMLStreamWriter
-class XmlBuilder(private val w: XMLStreamWriter) {
- fun el(path: String, lambda: XmlBuilder.() -> Unit = {}) {
+interface XmlBuilder {
+ fun el(path: String, lambda: XmlBuilder.() -> Unit = {})
+ fun el(path: String, content: String) {
+ el(path) {
+ text(content)
+ }
+ }
+ fun attr(namespace: String, name: String, value: String)
+ fun attr(name: String, value: String)
+ fun text(content: String)
+
+ companion object {
+ fun toString(root: String, f: XmlBuilder.() -> Unit): String {
+ val factory = XMLOutputFactory.newFactory()
+ val stream = StringWriter()
+ var writer = factory.createXMLStreamWriter(stream)
+ /**
+ * NOTE: commenting out because it wasn't obvious how to output the
+ * "standalone = 'yes' directive". Manual forge was therefore preferred.
+ */
+ stream.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>")
+ XmlStreamBuilder(writer).el(root) {
+ this.f()
+ }
+ writer.writeEndDocument()
+ return stream.buffer.toString()
+ }
+
+ fun toDom(root: String, schema: String?, f: XmlBuilder.() -> Unit): Document {
+ val factory = DocumentBuilderFactory.newInstance();
+ factory.isNamespaceAware = true
+ val builder = factory.newDocumentBuilder();
+ val doc = builder.newDocument();
+ doc.setXmlVersion("1.0")
+ doc.setXmlStandalone(true)
+ val root = doc.createElementNS(schema, root)
+ doc.appendChild(root);
+ XmlDOMBuilder(doc, schema, root).f()
+ doc.normalize()
+ return doc
+ }
+ }
+}
+
+private class XmlStreamBuilder(private val w: XMLStreamWriter): XmlBuilder {
+ override fun el(path: String, lambda: XmlBuilder.() -> Unit) {
path.splitToSequence('/').forEach {
w.writeStartElement(it)
}
@@ -39,35 +84,42 @@ class XmlBuilder(private val w: XMLStreamWriter) {
}
}
- fun el(path: String, content: String) {
- el(path) {
- text(content)
- }
+ override fun attr(namespace: String, name: String, value: String) {
+ w.writeAttribute(namespace, name, value)
}
- fun attr(name: String, value: String) {
+ override fun attr(name: String, value: String) {
w.writeAttribute(name, value)
}
- fun text(content: String) {
+ override fun text(content: String) {
w.writeCharacters(content)
}
}
-fun constructXml(root: String, f: XmlBuilder.() -> Unit): String {
- val factory = XMLOutputFactory.newFactory()
- val stream = StringWriter()
- var writer = factory.createXMLStreamWriter(stream)
- /**
- * NOTE: commenting out because it wasn't obvious how to output the
- * "standalone = 'yes' directive". Manual forge was therefore preferred.
- */
- stream.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>")
- XmlBuilder(writer).el(root) {
- this.f()
- }
- writer.writeEndDocument()
- return stream.buffer.toString()
+private class XmlDOMBuilder(private val doc: Document, private val schema: String?, private var node: Element): XmlBuilder {
+ override fun el(path: String, lambda: XmlBuilder.() -> Unit) {
+ val current = node
+ path.splitToSequence('/').forEach {
+ val new = doc.createElementNS(schema, it)
+ node.appendChild(new)
+ node = new
+ }
+ lambda()
+ node = current
+ }
+
+ override fun attr(namespace: String, name: String, value: String) {
+ node.setAttributeNS(namespace, name, value)
+ }
+
+ override fun attr(name: String, value: String) {
+ node.setAttribute(name, value)
+ }
+
+ override fun text(content: String) {
+ node.appendChild(doc.createTextNode(content));
+ }
}
class DestructionError(m: String) : Exception(m)
diff --git a/ebics/src/test/kotlin/XmlCombinatorsTest.kt b/ebics/src/test/kotlin/XmlCombinatorsTest.kt
index 8d722414..396de451 100644
--- a/ebics/src/test/kotlin/XmlCombinatorsTest.kt
+++ b/ebics/src/test/kotlin/XmlCombinatorsTest.kt
@@ -19,49 +19,58 @@
import org.junit.Test
import tech.libeufin.ebics.XmlBuilder
-import tech.libeufin.ebics.constructXml
+import tech.libeufin.ebics.XMLUtil
import kotlin.test.assertEquals
class XmlCombinatorsTest {
+ fun testBuilder(expected: String, root: String, builder: XmlBuilder.() -> Unit) {
+ val toString = XmlBuilder.toString(root, builder)
+ val toDom = XmlBuilder.toDom(root, null, builder)
+ assertEquals(expected, toString)
+ assertEquals(expected, XMLUtil.convertDomToBytes(toDom).toString(Charsets.UTF_8))
+ }
@Test
fun testWithModularity() {
fun module(base: XmlBuilder) {
base.el("module")
}
- val s = constructXml("root") {
+ testBuilder(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><root><module/></root>",
+ "root"
+ ) {
module(this)
}
- println(s)
- assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><root><module/></root>", s)
}
@Test
fun testWithIterable() {
- val s = constructXml("iterable") {
+ testBuilder(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><iterable><endOfDocument><e1><e11>111</e11></e1><e2><e22>222</e22></e2><e3><e33>333</e33></e3><e4><e44>444</e44></e4><e5><e55>555</e55></e5><e6><e66>666</e66></e6><e7><e77>777</e77></e7><e8><e88>888</e88></e8><e9><e99>999</e99></e9><e10><e1010>101010</e1010></e10></endOfDocument></iterable>",
+ "iterable"
+ ) {
el("endOfDocument") {
for (i in 1..10)
- el("$i/$i$i", "$i$i$i")
+ el("e$i/e$i$i", "$i$i$i")
}
}
- println(s)
- assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><iterable><endOfDocument><1><11>111</11></1><2><22>222</22></2><3><33>333</33></3><4><44>444</44></4><5><55>555</55></5><6><66>666</66></6><7><77>777</77></7><8><88>888</88></8><9><99>999</99></9><10><1010>101010</1010></10></endOfDocument></iterable>", s)
}
@Test
fun testBasicXmlBuilding() {
- val s = constructXml("ebics:ebicsRequest") {
+ testBuilder(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><ebicsRequest version=\"H004\"><a><b><c attribute-of=\"c\"><d><e><f nested=\"true\"><g><h/></g></f></e></d></c></b></a><one_more/></ebicsRequest>",
+ "ebicsRequest"
+ ) {
attr("version", "H004")
el("a/b/c") {
attr("attribute-of", "c")
- el("//d/e/f//") {
+ el("d/e/f") {
attr("nested", "true")
- el("g/h/")
+ el("g/h")
}
}
- el("one more")
+ el("one_more")
}
- println(s)
- assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><ebics:ebicsRequest version=\"H004\"><a><b><c attribute-of=\"c\"><><><d><e><f><>< nested=\"true\"><g><h></></h></g></></></f></e></d></></></c></b></a><one more/></ebics:ebicsRequest>", s)
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
index fb9c03ce..946a1808 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -83,7 +83,7 @@ fun createPain001(
)
val zonedTimestamp = ZonedDateTime.ofInstant(initiationTimestamp, ZoneId.of("UTC"))
val amountWithoutCurrency: String = getAmountNoCurrency(amount)
- return constructXml("Document") {
+ return XmlBuilder.toString("Document") {
attr("xmlns", namespace.fullNamespace)
attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
attr("xsi:schemaLocation", "${namespace.fullNamespace} ${namespace.xsdFilename}")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
index 29a5e5e7..099669c1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
@@ -19,18 +19,21 @@
package tech.libeufin.nexus.ebics
import io.ktor.client.*
-import tech.libeufin.ebics.PreparedUploadData
-import tech.libeufin.ebics.XMLUtil
+import tech.libeufin.ebics.*
+import tech.libeufin.common.*
+import tech.libeufin.common.crypto.*
import tech.libeufin.ebics.ebics_h005.Ebics3Request
-import tech.libeufin.ebics.getNonce
-import tech.libeufin.ebics.getXmlDate
import tech.libeufin.nexus.BankPublicKeysFile
import tech.libeufin.nexus.ClientPrivateKeysFile
import tech.libeufin.nexus.EbicsSetupConfig
import tech.libeufin.nexus.logger
import java.math.BigInteger
-import java.time.Instant
+import java.time.*
+import java.time.format.*
import java.util.*
+import java.io.File
+import org.w3c.dom.*
+import javax.xml.datatype.XMLGregorianCalendar
import javax.xml.datatype.DatatypeFactory
/**
@@ -97,7 +100,6 @@ fun createEbics3DownloadTransferPhase(
return XMLUtil.convertDomToBytes(doc)
}
-
/**
* Creates the EBICS 3 document for the init phase of a download
* transaction.
@@ -109,58 +111,83 @@ fun createEbics3DownloadTransferPhase(
*/
fun createEbics3DownloadInitialization(
cfg: EbicsSetupConfig,
- bankkeys: BankPublicKeysFile,
+ bankKeys: BankPublicKeysFile,
clientKeys: ClientPrivateKeysFile,
whichDoc: SupportedDocument,
startDate: Instant? = null,
endDate: Instant? = null
): ByteArray {
val nonce = getNonce(128)
- val req = Ebics3Request.createForDownloadInitializationPhase(
- cfg.ebicsUserId,
- cfg.ebicsPartnerId,
- cfg.ebicsHostId,
- nonce,
- DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
- bankAuthPub = bankkeys.bank_authentication_public_key,
- bankEncPub = bankkeys.bank_encryption_public_key,
- myOrderParams = if (whichDoc == SupportedDocument.PAIN_002_LOGS) null else Ebics3Request.OrderDetails.BTDOrderParams().apply {
- service = Ebics3Request.OrderDetails.Service().apply {
- serviceName = when(whichDoc) {
- SupportedDocument.PAIN_002 -> "PSR"
- SupportedDocument.CAMT_052 -> "STM"
- SupportedDocument.CAMT_053 -> "EOP"
- SupportedDocument.CAMT_054 -> "REP"
- SupportedDocument.PAIN_002_LOGS -> "HAC"
- }
- scope = "CH"
- container = Ebics3Request.OrderDetails.Service.Container().apply {
- containerType = "ZIP"
+ val timestamp = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar())
+ val doc = XmlBuilder.toDom("ebicsRequest", "urn:org:ebics:H005") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H005")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
+ attr("http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation", "urn:org:ebics:H005 ebics_request_H005.xsd")
+ attr("Version", "H005")
+ attr("Revision", "1")
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("Nonce", nonce.toHexString())
+ el("Timestamp", timestamp.toXMLFormat() )
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ el("OrderDetails") {
+ if (whichDoc == SupportedDocument.PAIN_002_LOGS) {
+ el("AdminOrderType", "HAC")
+ } else {
+ el("AdminOrderType", "BTD")
+ el("BTDOrderParams") {
+ el("Service") {
+ el("ServiceName", when(whichDoc) {
+ SupportedDocument.PAIN_002 -> "PSR"
+ SupportedDocument.CAMT_052 -> "STM"
+ SupportedDocument.CAMT_053 -> "EOP"
+ SupportedDocument.CAMT_054 -> "REP"
+ SupportedDocument.PAIN_002_LOGS -> "HAC"
+ })
+ val (msg_value, msg_version) = when(whichDoc) {
+ SupportedDocument.PAIN_002 -> Pair("pain.002", "10")
+ SupportedDocument.CAMT_052 -> Pair("pain.052", "08")
+ SupportedDocument.CAMT_053 -> Pair("pain.053", "08")
+ SupportedDocument.CAMT_054 -> Pair("camt.054", "08")
+ SupportedDocument.PAIN_002_LOGS -> throw Exception("HAC (--only-logs) not available in EBICS 3")
+ }
+ el("Scope", "CH")
+ el("Container") {
+ attr("containerType", "ZIP")
+ }
+ el("MsgName") {
+ attr("version", msg_version)
+ text(msg_value)
+ }
+ }
+ }
+ }
}
- messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
- val (msg_value, msg_version) = when(whichDoc) {
- SupportedDocument.PAIN_002 -> Pair("pain.002", "10")
- SupportedDocument.CAMT_052 -> Pair("pain.052", "08")
- SupportedDocument.CAMT_053 -> Pair("pain.053", "08")
- SupportedDocument.CAMT_054 -> Pair("camt.054", "08")
- SupportedDocument.PAIN_002_LOGS -> throw Exception("HAC (--only-logs) not available in EBICS 3")
+ el("BankPubKeyDigests") {
+ el("Authentication") {
+ attr("Version", "X002")
+ attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
+ text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key).encodeBase64())
+ }
+ el("Encryption") {
+ attr("Version", "E002")
+ attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
+ text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64())
}
- value = msg_value
- version = msg_version
}
+ el("SecurityMedium", "0000")
}
- if (startDate != null) {
- Ebics3Request.DateRange().apply {
- start = getXmlDate(startDate)
- end = getXmlDate(endDate ?: Instant.now())
- }
+ el("mutable") {
+ el("TransactionPhase", "Initialisation")
}
}
- )
- val doc = XMLUtil.convertJaxbToDocument(
- req,
- withSchemaLocation = "urn:org:ebics:H005 ebics_request_H005.xsd"
- )
+ el("AuthSignature")
+ el("body")
+ }
XMLUtil.signEbicsDocument(
doc,
clientKeys.authentication_private_key,