gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: Importing Nexus from refactoring branc


From: gnunet
Subject: [libeufin] branch master updated: Importing Nexus from refactoring branch.
Date: Wed, 18 Oct 2023 15:41:15 +0200

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new e670ab6a Importing Nexus from refactoring branch.
e670ab6a is described below

commit e670ab6a3ab5bf2851eed8f4fb58405967b812f0
Author: MS <ms@taler.net>
AuthorDate: Wed Oct 18 15:40:21 2023 +0200

    Importing Nexus from refactoring branch.
---
 nexus/build.gradle                                 | 118 ++++
 nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt  | 762 +++++++++++++++++++++
 .../main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt | 384 +++++++++++
 .../kotlin/tech/libeufin/nexus/ebics/Ebics2.kt     | 261 +++++++
 nexus/src/main/resources/logback.xml               |  23 +
 nexus/src/test/kotlin/Common.kt                    |  50 ++
 nexus/src/test/kotlin/ConfigLoading.kt             |  35 +
 nexus/src/test/kotlin/Ebics.kt                     | 122 ++++
 nexus/src/test/kotlin/Keys.kt                      |  88 +++
 nexus/src/test/kotlin/MySerializers.kt             |  32 +
 settings.gradle                                    |   2 +-
 util/src/main/kotlin/Ebics.kt                      |   8 +-
 12 files changed, 1880 insertions(+), 5 deletions(-)

diff --git a/nexus/build.gradle b/nexus/build.gradle
new file mode 100644
index 00000000..b6717712
--- /dev/null
+++ b/nexus/build.gradle
@@ -0,0 +1,118 @@
+plugins {
+    id 'kotlin'
+    id 'java'
+    id 'application'
+    id 'org.jetbrains.kotlin.jvm'
+    id "com.github.johnrengelman.shadow" version "5.2.0"
+    id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.22'
+}
+
+sourceSets {
+    main.java.srcDirs = ['src/main/kotlin']
+}
+
+task installToPrefix(type: Copy) {
+    dependsOn(installShadowDist)
+    from("build/install/nexus-shadow") {
+        include("**/libeufin-nexus")
+        include("**/*.jar")
+    }
+    into "${project.findProperty('prefix') ?: '/tmp'}" // reads from 
-Pprefix=foo, defaults to /tmp
+}
+
+apply plugin: 'kotlin-kapt'
+
+sourceCompatibility = '11'
+targetCompatibility = '11'
+version = rootProject.version
+
+compileKotlin {
+    kotlinOptions {
+        jvmTarget = '11'
+    }
+}
+
+compileTestKotlin {
+    kotlinOptions {
+        jvmTarget = '11'
+    }
+}
+
+dependencies {
+    // Core language libraries
+    implementation 
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt'
+
+    // LibEuFin util library
+    implementation project(":util")
+
+    // Logging
+    implementation 'ch.qos.logback:logback-classic:1.4.5'
+
+    // XML parsing/binding and encryption
+    implementation "javax.xml.bind:jaxb-api:2.3.0"
+    implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1"
+    implementation 'org.apache.santuario:xmlsec:2.2.2'
+
+    // Compression
+    implementation group: 'org.apache.commons', name: 'commons-compress', 
version: '1.21'
+
+    // Command line parsing
+    implementation('com.github.ajalt:clikt:2.8.0')
+
+    // Database connection driver
+    implementation group: 'org.xerial', name: 'sqlite-jdbc', version: 
'3.36.0.1'
+    implementation 'org.postgresql:postgresql:42.2.23.jre7'
+
+    // Ktor, an HTTP client and server library (no need for nexus-setup)
+    implementation "io.ktor:ktor-server-core:$ktor_version"
+    implementation "io.ktor:ktor-server-content-negotiation:$ktor_version"
+    implementation "io.ktor:ktor-server-status-pages:$ktor_version"
+    implementation "io.ktor:ktor-client-apache:$ktor_version"
+    implementation "io.ktor:ktor-client-auth:$ktor_version"
+    implementation "io.ktor:ktor-server-netty:$ktor_version"
+
+    // Brings the call-logging library too.
+    implementation "io.ktor:ktor-server-test-host:$ktor_version"
+    implementation "io.ktor:ktor-auth:$ktor_auth_version"
+
+    // PDF generation
+    implementation 'com.itextpdf:itext7-core:7.1.16'
+
+    // UNIX domain sockets support (used to connect to PostgreSQL)
+    implementation 'com.kohlschutter.junixsocket:junixsocket-core:2.6.2'
+
+    // Serialization
+    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
+    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
+
+    // Unit testing
+    // testImplementation 'junit:junit:4.13.2'
+    // From 
https://docs.gradle.org/current/userguide/java_testing.html#sec:java_testing_basics:
+    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
+    testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21'
+    testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
+    testImplementation 'io.ktor:ktor-client-mock:2.2.4'
+    testImplementation 'com.kohlschutter.junixsocket:junixsocket-core:2.6.2'
+}
+
+test {
+    useJUnit()
+    failFast = true
+    testLogging.showStandardStreams = false
+}
+
+application {
+    mainClassName = "tech.libeufin.nexus.MainKt"
+    applicationName = "libeufin-nexus"
+    applicationDefaultJvmArgs = ['-Djava.net.preferIPv6Addresses=true']
+}
+
+jar {
+    manifest {
+        attributes "Main-Class": "tech.libeufin.nexus.MainKt"
+    }
+}
+
+run {
+    standardInput = System.in
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
new file mode 100644
index 00000000..abd5044e
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -0,0 +1,762 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 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/>
+ */
+
+/**
+ * This file runs the main logic of nexus-setup.  This tool is
+ * responsible for reading configuration values about an EBICS
+ * subscriber and preparing the key material for further communication
+ * with the bank.
+ */
+
+package tech.libeufin.nexus
+import ConfigSource
+import TalerConfig
+import TalerConfigError
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.subcommands
+import com.github.ajalt.clikt.parameters.options.flag
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.options.versionOption
+import io.ktor.client.*
+import io.ktor.util.*
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.KSerializer
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.File
+import kotlin.system.exitProcess
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import net.taler.wallet.crypto.Base32Crockford
+import org.slf4j.event.Level
+import tech.libeufin.nexus.ebics.*
+import tech.libeufin.util.*
+import tech.libeufin.util.ebics_h004.EbicsTypes
+import java.security.interfaces.RSAPrivateCrtKey
+import java.security.interfaces.RSAPublicKey
+import java.time.Instant
+import kotlin.reflect.typeOf
+
+val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin-nexus", "libeufin-nexus")
+val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus.Main")
+val myJson = Json {
+    this.serializersModule = SerializersModule {
+        contextual(RSAPrivateCrtKey::class) { RSAPrivateCrtKeySerializer }
+        contextual(RSAPublicKey::class) { RSAPublicKeySerializer }
+    }
+}
+
+/**
+ * Keeps all the options of the ebics-setup subcommand.  The
+ * caller has to handle TalerConfigError if values are missing.
+ * If even one of the fields could not be instantiated, then
+ * throws TalerConfigError.
+ */
+class EbicsSetupConfig(config: TalerConfig) {
+    // abstracts the section name.
+    private val ebicsSetupRequireString = { option: String ->
+        config.requireString("nexus-ebics", option)
+    }
+    // debug utility to inspect what was loaded.
+    fun _dump() {
+        this.javaClass.declaredFields.forEach {
+            println("cfg obj: ${it.name} -> ${it.get(this)}")
+        }
+    }
+    /**
+     * The bank's currency.
+     */
+    val currency = ebicsSetupRequireString("currency")
+    /**
+     * The bank base URL.
+     */
+    val hostBaseUrl = ebicsSetupRequireString("host_base_url")
+    /**
+     * The bank EBICS host ID.
+     */
+    val ebicsHostId = ebicsSetupRequireString("host_id")
+    /**
+     * EBICS user ID.
+     */
+    val ebicsUserId = ebicsSetupRequireString("user_id")
+    /**
+     * EBICS partner ID.
+     */
+    val ebicsPartnerId = ebicsSetupRequireString("partner_id")
+    /**
+     * EBICS system ID (is this optional?).
+     */
+    val ebicsSystemId = ebicsSetupRequireString("system_id")
+    /**
+     * Bank account name, as given by the bank.  It
+     * can be an IBAN or even any alphanumeric value.
+     */
+    val accountNumber = ebicsSetupRequireString("account_number")
+    /**
+     * Filename where we store the bank public keys.
+     */
+    val bankPublicKeysFilename = 
ebicsSetupRequireString("bank_public_keys_file")
+    /**
+     * Filename where we store our private keys.
+     */
+    val clientPrivateKeysFilename = 
ebicsSetupRequireString("client_private_keys_file")
+    /**
+     * Filename where we store the bank account main information.
+     */
+    val bankAccountMetadataFilename = 
ebicsSetupRequireString("account_meta_data_file")
+    /**
+     * A name that identifies the EBICS and ISO20022 flavour
+     * that Nexus should honor in the communication with the
+     * bank.
+     */
+    val bankDialect: String = ebicsSetupRequireString("bank_dialect").run {
+        if (this != "postfinance") throw Exception("Only 'postfinance' dialect 
is supported.")
+        return@run this
+    }
+}
+
+/**
+ * Converts base 32 representation of RSA public keys and vice versa.
+ */
+object RSAPublicKeySerializer : KSerializer<RSAPublicKey> {
+    override val descriptor: SerialDescriptor =
+        PrimitiveSerialDescriptor("RSAPublicKey", PrimitiveKind.STRING)
+    override fun serialize(encoder: Encoder, value: RSAPublicKey) {
+        encoder.encodeString(Base32Crockford.encode(value.encoded))
+    }
+
+    // Caller must handle exceptions here.
+    override fun deserialize(decoder: Decoder): RSAPublicKey {
+        val fieldValue = decoder.decodeString()
+        val bytes = Base32Crockford.decode(fieldValue)
+        return CryptoUtil.loadRsaPublicKey(bytes)
+    }
+}
+
+/**
+ * Converts base 32 representation of RSA private keys and vice versa.
+ */
+object RSAPrivateCrtKeySerializer : KSerializer<RSAPrivateCrtKey> {
+    override val descriptor: SerialDescriptor =
+        PrimitiveSerialDescriptor("RSAPrivateCrtKey", PrimitiveKind.STRING)
+    override fun serialize(encoder: Encoder, value: RSAPrivateCrtKey) {
+        encoder.encodeString(Base32Crockford.encode(value.encoded))
+    }
+
+    // Caller must handle exceptions here.
+    override fun deserialize(decoder: Decoder): RSAPrivateCrtKey {
+        val fieldValue = decoder.decodeString()
+        val bytes = Base32Crockford.decode(fieldValue)
+        return CryptoUtil.loadRsaPrivateKey(bytes)
+    }
+}
+
+/**
+ * Structure of the file that holds the bank account
+ * metadata.
+ */
+@Serializable
+data class BankAccountMetadataFile(
+    val account_holder_iban: String,
+    val bank_code: String?,
+    val account_holder_name: String
+)
+
+/**
+ * Structure of the JSON file that contains the client
+ * private keys on disk.
+ */
+@Serializable
+data class ClientPrivateKeysFile(
+    // FIXME: centralize the @Contextual use.
+    @Contextual val signature_private_key: RSAPrivateCrtKey,
+    @Contextual val encryption_private_key: RSAPrivateCrtKey,
+    @Contextual val authentication_private_key: RSAPrivateCrtKey,
+    var submitted_ini: Boolean,
+    var submitted_hia: Boolean
+)
+
+/**
+ * Structure of the JSON file that contains the bank
+ * public keys on disk.
+ */
+@Serializable
+data class BankPublicKeysFile(
+    @Contextual val bank_encryption_public_key: RSAPublicKey,
+    @Contextual val bank_authentication_public_key: RSAPublicKey,
+    var accepted: Boolean
+)
+/**
+ * Writes the JSON content to disk.  Used when we create or update
+ * keys and other metadata JSON content to disk.  WARNING: this overrides
+ * silently what's found under the given location!
+ *
+ * @param obj the class representing the JSON content to store to disk.
+ * @param location where to store `obj`
+ * @return true in case of success, false otherwise.
+ */
+inline fun <reified T> syncJsonToDisk(obj: T, location: String): Boolean {
+    val fileContent = try {
+        myJson.encodeToString(obj)
+    } catch (e: Exception) {
+        logger.error("Could not encode the input '${typeOf<T>()}' to JSON, 
detail: ${e.message}")
+        return false
+    }
+    try {
+        File(location).writeText(fileContent)
+    } catch (e: Exception) {
+        logger.error("Could not write JSON content at $location, detail: 
${e.message}")
+        return false
+    }
+    return true
+}
+fun generateNewKeys(): ClientPrivateKeysFile =
+    ClientPrivateKeysFile(
+        authentication_private_key = 
CryptoUtil.generateRsaKeyPair(2048).private,
+        encryption_private_key = CryptoUtil.generateRsaKeyPair(2048).private,
+        signature_private_key = CryptoUtil.generateRsaKeyPair(2048).private,
+        submitted_hia = false,
+        submitted_ini = false
+)
+/**
+ * Conditionally generates the client private keys and stores them
+ * to disk, if the file does not exist already.  Does nothing if the
+ * file exists.
+ *
+ * @param filename keys file location
+ * @return true if the keys file existed already or its creation
+ *         went through, false for any error.
+ */
+fun maybeCreatePrivateKeysFile(filename: String): Boolean {
+    val f = File(filename)
+    // NOT overriding any file at the wanted location.
+    if (f.exists()) {
+        logger.debug("Private key file found at: $filename.")
+        return true
+    }
+    val newKeys = generateNewKeys()
+    if (!syncJsonToDisk(newKeys, filename))
+        return false
+    logger.info("New client keys created at: $filename")
+    return true
+}
+
+/**
+ * Load the bank keys file from disk.
+ *
+ * @param location the keys file location.
+ * @return the internal JSON representation of the keys file,
+ *         or null on failures.
+ */
+fun loadBankKeys(location: String): BankPublicKeysFile? {
+    val f = File(location)
+    if (!f.exists()) {
+        logger.error("Could not find the bank keys file at: $location")
+        return null
+    }
+    val fileContent = try {
+        f.readText() // read from disk.
+    } catch (e: Exception) {
+        logger.error("Could not read the bank keys file from disk, detail: 
${e.message}")
+        return null
+    }
+    return try {
+        myJson.decodeFromString(fileContent) // Parse into JSON.
+    } catch (e: Exception) {
+        logger.error(e.message)
+        @OptIn(InternalAPI::class) // enables message below.
+        logger.error(e.rootCause?.message) // actual useful message mentioning 
failing fields
+        return null
+    }
+}
+
+/**
+ * Load the client keys file from disk.
+ *
+ * @param location the keys file location.
+ * @return the internal JSON representation of the keys file,
+ *         or null on failures.
+ */
+fun loadPrivateKeysFromDisk(location: String): ClientPrivateKeysFile? {
+    val f = File(location)
+    if (!f.exists()) {
+        logger.error("Could not find the private keys file at: $location")
+        return null
+    }
+    val fileContent = try {
+        f.readText() // read from disk.
+    } catch (e: Exception) {
+        logger.error("Could not read private keys from disk, detail: 
${e.message}")
+        return null
+    }
+    return try {
+        myJson.decodeFromString(fileContent) // Parse into JSON.
+    } catch (e: Exception) {
+        logger.error(e.message)
+        @OptIn(InternalAPI::class) // enables message below.
+        logger.error(e.rootCause?.message) // actual useful message mentioning 
failing fields
+        return null
+    }
+}
+
+/**
+ * Obtains the client private keys, regardless of them being
+ * created for the first time, or read from an existing file
+ * on disk.
+ *
+ * @param location path to the file that contains the keys.
+ * @return true if the operation succeeds, false otherwise.
+ */
+fun preparePrivateKeys(location: String): ClientPrivateKeysFile? {
+    if (!maybeCreatePrivateKeysFile(location)) {
+        logger.error("Could not create client keys at $location")
+        exitProcess(1)
+    }
+    return loadPrivateKeysFromDisk(location) // loads what found at location.
+}
+
+/**
+ * Expresses the type of keying message that the user wants
+ * to send to the bank.
+ */
+enum class KeysOrderType {
+    INI,
+    HIA,
+    HPB
+}
+
+/**
+ * @return the "this" string with a space every two characters.
+ */
+fun String.spaceEachTwo() =
+    buildString {
+        this@spaceEachTwo.forEachIndexed { pos, c ->
+            when {
+                (pos == 0) -> this.append(c)
+                (pos % 2 == 0) -> this.append(" $c")
+                else -> this.append(c)
+            }
+        }
+    }
+
+/**
+ * Asks the user to accept the bank public keys.
+ *
+ * @param bankKeys bank public keys, in format stored on disk.
+ * @return true if the user accepted, false otherwise.
+ */
+fun askUserToAcceptKeys(bankKeys: BankPublicKeysFile): Boolean {
+    val encHash = 
CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).toHexString()
+    val authHash = 
CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key).toHexString()
+    println("The bank has the following keys, type 'yes, accept' to accept 
them..\n")
+    println("Encryption key: ${encHash.spaceEachTwo()}")
+    println("Authentication key: ${authHash.spaceEachTwo()}")
+    val userResponse: String? = readlnOrNull()
+    if (userResponse == "yes, accept")
+        return true
+    return false
+}
+
+/**
+ * Parses the HPB response and stores the bank keys as "NOT accepted" to disk.
+ *
+ * @param cfg used to get the location of the bank keys file.
+ * @param bankKeys bank response to the HPB message.
+ * @return true if the keys were stored to disk (as "not accepted"),
+ *         false if the storage failed or the content was invalid.
+ */
+private fun handleHpbResponse(
+    cfg: EbicsSetupConfig,
+    bankKeys: EbicsKeyManagementResponseContent
+): Boolean {
+    val hpbBytes = bankKeys.orderData // silences compiler.
+    if (hpbBytes == null) {
+        logger.error("HPB content not found in a EBICS response with 
successful return codes.")
+        return false
+    }
+    val hpbObj = try {
+        parseEbicsHpbOrder(hpbBytes)
+    }
+    catch (e: Exception) {
+        logger.error("HPB response content seems invalid.")
+        return false
+    }
+    val encPub = try {
+        CryptoUtil.loadRsaPublicKey(hpbObj.encryptionPubKey.encoded)
+    } catch (e: Exception) {
+        logger.error("Could not import bank encryption key from HPB response, 
detail: ${e.message}")
+        return false
+    }
+    val authPub = try {
+        CryptoUtil.loadRsaPublicKey(hpbObj.authenticationPubKey.encoded)
+    } catch (e: Exception) {
+        logger.error("Could not import bank authentication key from HPB 
response, detail: ${e.message}")
+        return false
+    }
+    val json = BankPublicKeysFile(
+        bank_authentication_public_key = authPub,
+        bank_encryption_public_key = encPub,
+        accepted = false
+    )
+    if (!syncJsonToDisk(json, cfg.bankPublicKeysFilename)) {
+        logger.error("Failed to persist the bank keys to disk at: 
${cfg.bankPublicKeysFilename}")
+        return false
+    }
+    return true
+}
+
+/**
+ * Collects all the steps from generating the message, to
+ * sending it to the bank, and finally updating the state
+ * on disk according to the response.
+ *
+ * @param cfg handle to the configuration.
+ * @param privs bundle of all the private keys of the client.
+ * @param client the http client that requests to the bank.
+ * @param orderType INI or HIA.
+ * @param autoAcceptBankKeys only given in case of HPB.  Expresses
+ *        the --auto-accept-key CLI flag.
+ * @return true if the message fulfilled its purpose AND the state
+ *         on disk was accordingly updated, or false otherwise.
+ */
+suspend fun doKeysRequestAndUpdateState(
+    cfg: EbicsSetupConfig,
+    privs: ClientPrivateKeysFile,
+    client: HttpClient,
+    orderType: KeysOrderType
+): Boolean {
+    val req = when(orderType) {
+        KeysOrderType.INI -> generateIniMessage(cfg, privs)
+        KeysOrderType.HIA -> generateHiaMessage(cfg, privs)
+        KeysOrderType.HPB -> generateHpbMessage(cfg, privs)
+    }
+    val xml = client.postToBank(cfg.hostBaseUrl, req)
+    if (xml == null) {
+        logger.error("Could not POST the ${orderType.name} message to the 
bank")
+        return false
+    }
+    val ebics = parseKeysMgmtResponse(privs.encryption_private_key, xml)
+    if (ebics == null) {
+        logger.error("Could not get any EBICS from the bank ${orderType.name} 
response ($xml).")
+        return false
+    }
+    if (ebics.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
+        logger.error("EBICS ${orderType.name} failed with code: 
${ebics.technicalReturnCode}")
+        return false
+    }
+    if (ebics.bankReturnCode != EbicsReturnCode.EBICS_OK) {
+        logger.error("EBICS ${orderType.name} reached the bank, but could not 
be fulfilled, error code: ${ebics.bankReturnCode}")
+        return false
+    }
+
+    when(orderType) {
+        KeysOrderType.INI -> privs.submitted_ini = true
+        KeysOrderType.HIA -> privs.submitted_hia = true
+        KeysOrderType.HPB -> return handleHpbResponse(cfg, ebics)
+    }
+    if (!syncJsonToDisk(privs, cfg.clientPrivateKeysFilename)) {
+        logger.error("Could not update the ${orderType.name} state on disk")
+        return false
+    }
+    return true
+}
+
+/**
+ * Abstracts (part of) the IBAN extraction from an HTD response.
+ */
+private fun maybeExtractIban(accountNumberList: 
List<EbicsTypes.AbstractAccountNumber>): String? =
+    accountNumberList.filterIsInstance<EbicsTypes.GeneralAccountNumber>().find 
{ it.international }?.value
+
+/**
+ * Abstracts (part of) the BIC extraction from an HTD response.
+ */
+private fun maybeExtractBic(bankCodes: List<EbicsTypes.AbstractBankCode>): 
String? =
+    bankCodes.filterIsInstance<EbicsTypes.GeneralBankCode>().find { 
it.international }?.value
+
+/**
+ * Mere collector of the PDF generation steps.  Fails the
+ * process if a problem occurs.
+ *
+ * @param privs client private keys.
+ * @param cfg configuration handle.
+ */
+private fun makePdf(privs: ClientPrivateKeysFile, cfg: EbicsSetupConfig) {
+    val pdf = generateKeysPdf(privs, cfg)
+    val pdfFile = 
File("/tmp/libeufin-nexus-keys-${Instant.now().epochSecond}.pdf")
+    if (pdfFile.exists()) {
+        logger.error("PDF file exists already at: ${pdfFile.path}, not 
overriding it")
+        exitProcess(1)
+    }
+    try {
+        pdfFile.writeBytes(pdf)
+    } catch (e: Exception) {
+        logger.error("Could not write PDF to ${pdfFile}, detail: ${e.message}")
+        exitProcess(1)
+    }
+    println("PDF file with keys hex encoding created at: $pdfFile")
+}
+
+/**
+ * Mere collector of the steps to load and parse the config.
+ *
+ * @param configFile location of the configuration entry point.
+ * @return internal representation of the configuration.
+ */
+private fun extractConfig(configFile: String?): EbicsSetupConfig {
+    val config = TalerConfig(NEXUS_CONFIG_SOURCE)
+    try {
+        config.load(configFile)
+    } catch (e: Exception) {
+        logger.error("Could not load configuration from ${configFile}, detail: 
${e.message}")
+        exitProcess(1)
+    }
+    // Checking the config.
+    val cfg = try {
+        EbicsSetupConfig(config)
+    } catch (e: TalerConfigError) {
+        logger.error(e.message)
+        exitProcess(1)
+    }
+    return cfg
+}
+
+private fun findIban(maybeList: List<EbicsTypes.AccountInfo>?): String? {
+    if (maybeList == null) {
+        logger.warn("Looking for IBAN: bank did not give any account list for 
us.")
+        return null
+    }
+    if (maybeList.size != 1) {
+        logger.warn("Looking for IBAN: bank gave account list, but it was not 
a singleton.")
+        return null
+    }
+    val accountNumberList = maybeList[0].accountNumberList
+    if (accountNumberList == null) {
+        logger.warn("Bank gave account list, but no IBAN list of found.")
+        return null
+    }
+    if (accountNumberList.size != 1) {
+        logger.warn("Bank gave account list, but IBAN list was not singleton.")
+        return null
+    }
+    return maybeExtractIban(accountNumberList)
+}
+private fun findBic(maybeList: List<EbicsTypes.AccountInfo>?): String? {
+    if (maybeList == null) {
+        logger.warn("Looking for BIC: bank did not give any account list for 
us.")
+        return null
+    }
+    if (maybeList.size != 1) {
+        logger.warn("Looking for BIC: bank gave account list, but it was not a 
singleton.")
+        return null
+    }
+    val bankCodeList = maybeList[0].bankCodeList
+    if (bankCodeList == null) {
+        logger.warn("Bank gave account list, but no BIC list of found.")
+        return null
+    }
+    if (bankCodeList.size != 1) {
+        logger.warn("Bank gave account list, but BIC list was not singleton.")
+        return null
+    }
+    return maybeExtractBic(bankCodeList)
+}
+
+/**
+ * CLI class implementing the "ebics-setup" subcommand.
+ */
+class EbicsSetup: CliktCommand() {
+    private val configFile by option(
+        "--config", "-c",
+        help = "set the configuration file"
+    )
+    private val checkFullConfig by option(
+        help = "checks config values of ALL the subcommands"
+    ).flag(default = false)
+    private val forceKeysResubmission by option(
+        help = "resubmits all the keys to the bank"
+    ).flag(default = false)
+    private val autoAcceptKeys by option(
+        help = "accepts the bank keys without the user confirmation"
+    ).flag(default = false)
+    private val generateRegistrationPdf by option(
+        help = "generates the PDF with the client public keys to send to the 
bank"
+    ).flag(default = false)
+    private val showAssociatedAccounts by option(
+        help = "shows which bank accounts belong to the EBICS subscriber"
+    ).flag(default = false)
+
+    /**
+     * This function collects the main steps of setting up an EBICS access.
+     */
+    override fun run() {
+        val cfg = extractConfig(this.configFile)
+        if (checkFullConfig) {
+            throw NotImplementedError("--check-full-config flag not 
implemented")
+        }
+        // Config is sane.  Go (maybe) making the private keys.
+        val privsMaybe = preparePrivateKeys(cfg.clientPrivateKeysFilename)
+        if (privsMaybe == null) {
+            logger.error("Private keys preparation failed.")
+            exitProcess(1)
+        }
+        val httpClient = HttpClient()
+        // Privs exist.  Upload their pubs
+        val keysNotSub = !privsMaybe.submitted_ini || !privsMaybe.submitted_hia
+        runBlocking {
+            if ((!privsMaybe.submitted_ini) || forceKeysResubmission)
+                doKeysRequestAndUpdateState(cfg, privsMaybe, httpClient, 
KeysOrderType.INI).apply { if (!this) exitProcess(1) }
+            if ((!privsMaybe.submitted_hia) || forceKeysResubmission)
+                doKeysRequestAndUpdateState(cfg, privsMaybe, httpClient, 
KeysOrderType.HIA).apply { if (!this) exitProcess(1) }
+        }
+        // Reloading new state from disk if any upload (and therefore a disk 
write) actually took place
+        val haveSubmitted = forceKeysResubmission || keysNotSub
+        val privs = if (haveSubmitted) {
+            logger.info("Keys submitted to the bank, at ${cfg.hostBaseUrl}")
+            loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
+        } else privsMaybe
+        if (privs == null) {
+            logger.error("Could not reload private keys from disk after 
submission")
+            exitProcess(1)
+        }
+        // Really both must be submitted here.
+        if ((!privs.submitted_hia) || (!privs.submitted_ini)) {
+            logger.error("Cannot continue with non-submitted client keys.")
+            exitProcess(1)
+        }
+        // Eject PDF if the keys were submitted for the first time, or the 
user asked.
+        if (keysNotSub || generateRegistrationPdf) makePdf(privs, cfg)
+        // Checking if the bank keys exist on disk.
+        val bankKeysFile = File(cfg.bankPublicKeysFilename)
+        if (!bankKeysFile.exists()) { // FIXME: should this also check the 
content validity?
+            val areKeysOnDisk = runBlocking {
+                doKeysRequestAndUpdateState(
+                    cfg,
+                    privs,
+                    httpClient,
+                    KeysOrderType.HPB
+                )
+            }
+            if (!areKeysOnDisk) {
+                logger.error("Could not download bank keys.  Send client keys 
(and/or related PDF document with --generate-registration-pdf) to the bank.")
+                exitProcess(1)
+            }
+            logger.info("Bank keys stored at ${cfg.bankPublicKeysFilename}")
+        }
+        // bank keys made it to the disk, check if they're accepted.
+        val bankKeysMaybe = loadBankKeys(cfg.bankPublicKeysFilename)
+        if (bankKeysMaybe == null) {
+            logger.error("Although previous checks, could not load the bank 
keys file from: ${cfg.bankPublicKeysFilename}")
+            exitProcess(1)
+        }
+        /**
+         * The following block potentially updates the bank keys state
+         * on disk, if that's the first time that they become accepted.
+         * If so, finally reloads the bank keys file from disk.
+         */
+        val bankKeys = if (!bankKeysMaybe.accepted) {
+
+            if (autoAcceptKeys) bankKeysMaybe.accepted = true
+            else bankKeysMaybe.accepted = askUserToAcceptKeys(bankKeysMaybe)
+
+            if (!bankKeysMaybe.accepted) {
+                logger.error("Cannot continue without accepting the bank 
keys.")
+                exitProcess(1)
+            }
+
+            if (!syncJsonToDisk(bankKeysMaybe, cfg.bankPublicKeysFilename)) {
+                logger.error("Could not set bank keys as accepted on disk.")
+                exitProcess(1)
+            }
+            // Reloading after the disk write above.
+            loadBankKeys(cfg.bankPublicKeysFilename) ?: kotlin.run {
+                logger.error("Could not reload bank keys after disk write.")
+                exitProcess(1)
+            }
+        } else
+            bankKeysMaybe // keys were already accepted.
+
+        // Downloading the list of owned bank account(s).
+        val bankAccounts = runBlocking {
+            fetchBankAccounts(cfg, privs, bankKeys, httpClient)
+        }
+        if (bankAccounts == null) {
+            logger.error("Could not obtain the list of bank accounts from the 
bank.")
+            exitProcess(1)
+        }
+        logger.info("Subscriber's bank accounts fetched.")
+        // Now trying to extract whatever IBAN & BIC pair the bank gave in the 
response.
+        val foundIban: String? = 
findIban(bankAccounts.partnerInfo.accountInfoList)
+        val foundBic: String? = 
findBic(bankAccounts.partnerInfo.accountInfoList)
+        // _some_ IBAN & BIC _might_ have been found, compare it with the 
config.
+        if (foundIban == null)
+            logger.warn("Bank seems NOT to show any IBAN for our account.")
+        if (foundBic == null)
+            logger.warn("Bank seems NOT to show any BIC for our account.")
+        // Warn the user if instead one IBAN was found but that differs from 
the config.
+        if (foundIban != null && foundIban != cfg.accountNumber) {
+            logger.error("Bank has another IBAN for us: $foundIban, while 
config has: ${cfg.accountNumber}")
+            exitProcess(1)
+        }
+        // Users wants only _see_ the accounts, NOT checking values and 
returning here.
+        if (showAssociatedAccounts) {
+            println("Bank associates this account to the EBICS user 
${cfg.ebicsUserId}: IBAN: $foundIban, BIC: $foundBic, Name: 
${bankAccounts.userInfo.name}")
+            return
+        }
+        // No divergences were found, either because the config was right
+        // _or_ the bank didn't give any information.  Setting the account
+        // metadata accordingly.
+        val accountMetaData = BankAccountMetadataFile(
+            account_holder_name = bankAccounts.userInfo.name ?: "Account 
holder name not given",
+            account_holder_iban = foundIban ?: run iban@ {
+                logger.warn("Bank did not show any IBAN for us, defaulting to 
the one we configured.")
+                return@iban cfg.accountNumber },
+            bank_code = foundBic ?: run bic@ {
+                logger.warn("Bank did not show any BIC for us, setting it as 
null.")
+                return@bic null }
+        )
+        if (!syncJsonToDisk(accountMetaData, cfg.bankAccountMetadataFilename)) 
{
+            logger.error("Failed to persist bank account meta-data at: 
${cfg.bankAccountMetadataFilename}")
+            exitProcess(1)
+        }
+        println("setup ready")
+    }
+}
+
+/**
+ * Main CLI class that collects all the subcommands.
+ */
+class LibeufinNexusCommand : CliktCommand() {
+    init {
+        versionOption(getVersion())
+        subcommands(EbicsSetup())
+    }
+    override fun run() = Unit
+}
+
+fun main(args: Array<String>) {
+    LibeufinNexusCommand().main(args)
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt
new file mode 100644
index 00000000..2a6f7b9f
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt
@@ -0,0 +1,384 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 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/>
+ */
+
+/**
+ * This file collects the EBICS helpers in the most version-independent way.
+ * It tries therefore to make the helpers reusable across the EBICS versions 
2.x
+ * and 3.x.
+ */
+
+/**
+ * NOTE: it has been observed that even with a EBICS 3 server, it
+ * is still possible to exchange the keys via the EBICS 2.5 protocol.
+ * That is how this file does, but future versions should implement the
+ * EBICS 3 keying.
+ */
+
+package tech.libeufin.nexus.ebics
+
+import com.itextpdf.kernel.pdf.PdfDocument
+import com.itextpdf.kernel.pdf.PdfWriter
+import com.itextpdf.layout.Document
+import com.itextpdf.layout.element.AreaBreak
+import com.itextpdf.layout.element.Paragraph
+import io.ktor.client.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import tech.libeufin.nexus.BankPublicKeysFile
+import tech.libeufin.nexus.ClientPrivateKeysFile
+import tech.libeufin.nexus.EbicsSetupConfig
+import tech.libeufin.util.*
+import java.io.ByteArrayOutputStream
+import java.security.interfaces.RSAPrivateCrtKey
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.*
+
+/**
+ * Decrypts and decompresses the business payload that was
+ * transported within an EBICS message from the bank
+ *
+ * @param clientEncryptionKey client private encryption key, used to decrypt
+ *                            the transaction key.  The transaction key is the
+ *                            one actually used to encrypt the payload.
+ * @param encryptionInfo details related to the encrypted payload.
+ * @param chunks the several chunks that constitute the whole encrypted 
payload.
+ * @return the plain payload.  Errors throw, so the caller must handle those.
+ *
+ */
+fun decryptAndDecompressPayload(
+    clientEncryptionKey: RSAPrivateCrtKey,
+    encryptionInfo: DataEncryptionInfo,
+    chunks: List<String>
+): ByteArray {
+    val buf = StringBuilder()
+    chunks.forEach { buf.append(it) }
+    val decoded = Base64.getDecoder().decode(buf.toString())
+    val er = CryptoUtil.EncryptionResult(
+        encryptionInfo.transactionKey,
+        encryptionInfo.bankPubDigest,
+        decoded
+    )
+    val dataCompr = CryptoUtil.decryptEbicsE002(
+        er,
+        clientEncryptionKey
+    )
+    return EbicsOrderUtil.decodeOrderData(dataCompr)
+}
+
+/**
+ * POSTs the EBICS message to the bank.
+ *
+ * @param URL where the bank serves EBICS requests.
+ * @param msg EBICS message as raw string.
+ * @return the raw bank response, if the request made it to the
+ *         EBICS handler, or null otherwise.
+ */
+suspend fun HttpClient.postToBank(bankUrl: String, msg: String): String? {
+    val resp: HttpResponse = try {
+        this.post(urlString = bankUrl) {
+            expectSuccess = false // avoids exceptions on non-2xx statuses.
+            contentType(ContentType.Text.Xml)
+            setBody(msg)
+        }
+    }
+    catch (e: Exception) {
+        // hard error (network issue, invalid URL, ..)
+        tech.libeufin.nexus.logger.error("Could not POST to bank at: $bankUrl, 
detail: ${e.message}")
+        return null
+    }
+    // Bank was found, but the EBICS request wasn't served.
+    // Note: EBICS errors get _still_ 200 OK, so here the error
+    // _should_ not be related to EBICS.  404 for a wrong URL
+    // is one example.
+    if (resp.status != HttpStatusCode.OK) {
+        tech.libeufin.nexus.logger.error("Bank was found at $bankUrl, but 
EBICS wasn't served.  Response status: ${resp.status}, body: 
${resp.bodyAsText()}")
+        return null
+    }
+    return resp.bodyAsText()
+}
+
+/**
+ * Generate the PDF document with all the client public keys
+ * to be sent on paper to the bank.
+ */
+fun generateKeysPdf(
+    clientKeys: ClientPrivateKeysFile,
+    cfg: EbicsSetupConfig
+): ByteArray {
+    val po = ByteArrayOutputStream()
+    val pdfWriter = PdfWriter(po)
+    val pdfDoc = PdfDocument(pdfWriter)
+    val date = LocalDateTime.now()
+    val dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
+
+    fun formatHex(ba: ByteArray): String {
+        var out = ""
+        for (i in ba.indices) {
+            val b = ba[i]
+            if (i > 0 && i % 16 == 0) {
+                out += "\n"
+            }
+            out += java.lang.String.format("%02X", b)
+            out += " "
+        }
+        return out
+    }
+
+    fun writeCommon(doc: Document) {
+        doc.add(
+            Paragraph(
+                """
+            Datum: $dateStr
+            Host-ID: ${cfg.ebicsHostId}
+            User-ID: ${cfg.ebicsUserId}
+            Partner-ID: ${cfg.ebicsPartnerId}
+            ES version: A006
+        """.trimIndent()
+            )
+        )
+    }
+
+    fun writeKey(doc: Document, priv: RSAPrivateCrtKey) {
+        val pub = CryptoUtil.getRsaPublicFromPrivate(priv)
+        val hash = CryptoUtil.getEbicsPublicKeyHash(pub)
+        
doc.add(Paragraph("Exponent:\n${formatHex(pub.publicExponent.toByteArray())}"))
+        doc.add(Paragraph("Modulus:\n${formatHex(pub.modulus.toByteArray())}"))
+        doc.add(Paragraph("SHA-256 hash:\n${formatHex(hash)}"))
+    }
+
+    fun writeSigLine(doc: Document) {
+        doc.add(Paragraph("Ort / Datum: ________________"))
+        doc.add(Paragraph("Firma / Name: ________________"))
+        doc.add(Paragraph("Unterschrift: ________________"))
+    }
+
+    Document(pdfDoc).use {
+        it.add(Paragraph("Signaturschlüssel").setFontSize(24f))
+        writeCommon(it)
+        it.add(Paragraph("Öffentlicher Schlüssel (Public key for the 
electronic signature)"))
+        writeKey(it, clientKeys.signature_private_key)
+        it.add(Paragraph("\n"))
+        writeSigLine(it)
+        it.add(AreaBreak())
+
+        it.add(Paragraph("Authentifikationsschlüssel").setFontSize(24f))
+        writeCommon(it)
+        it.add(Paragraph("Öffentlicher Schlüssel (Public key for the 
identification and authentication signature)"))
+        writeKey(it, clientKeys.authentication_private_key)
+        it.add(Paragraph("\n"))
+        writeSigLine(it)
+        it.add(AreaBreak())
+
+        it.add(Paragraph("Verschlüsselungsschlüssel").setFontSize(24f))
+        writeCommon(it)
+        it.add(Paragraph("Öffentlicher Schlüssel (Public encryption key)"))
+        writeKey(it, clientKeys.encryption_private_key)
+        it.add(Paragraph("\n"))
+        writeSigLine(it)
+    }
+    pdfWriter.flush()
+    return po.toByteArray()
+}
+
+/**
+ * POSTs raw EBICS XML to the bank and checks the two return codes:
+ * EBICS- and bank-technical.
+ *
+ * @param clientKeys client keys, used to sign the request.
+ * @param bankKeys bank keys, used to decrypt and validate the response.
+ * @param xmlBody raw EBICS request in XML.
+ * @param withEbics3 true in case the communication is EBICS 3, false 
otherwise.
+ * @param tolerateEbicsReturnCode EBICS technical return code that may be 
accepted
+ *                                instead of EBICS_OK.  That is the case of 
EBICS_DOWNLOAD_POSTPROCESS_DONE
+ *                                along download receipt phases.
+ * @param tolerateBankReturnCode Business return code that may be accepted 
instead of
+ *                               EBICS_OK.  Typically, 
EBICS_NO_DOWNLOAD_DATA_AVAILABLE is tolerated
+ *                               when asking for new incoming payments.
+ * @return the internal representation of an EBICS response IF both return 
codes
+ *         were EBICS_OK, or null otherwise.
+ */
+suspend fun postEbicsAndCheckReturnCodes(
+    client: HttpClient,
+    cfg: EbicsSetupConfig,
+    bankKeys: BankPublicKeysFile,
+    xmlReq: String,
+    isEbics3: Boolean,
+    tolerateEbicsReturnCode: EbicsReturnCode? = null,
+    tolerateBankReturnCode: EbicsReturnCode? = null
+): EbicsResponseContent? {
+    val respXml = client.postToBank(cfg.hostBaseUrl, xmlReq)
+    if (respXml == null) {
+        tech.libeufin.nexus.logger.error("EBICS init phase failed.  Aborting 
the HTD operation.")
+        return null
+    }
+    val respObj: EbicsResponseContent = parseAndValidateEbicsResponse(
+        bankKeys,
+        respXml,
+        isEbics3
+    ) ?: return null // helper logged the cause already.
+
+    var isEbicsCodeTolerated = false
+    if (tolerateEbicsReturnCode != null)
+       isEbicsCodeTolerated = respObj.technicalReturnCode == 
tolerateEbicsReturnCode
+
+    // EBICS communication error.
+    if ((respObj.technicalReturnCode != EbicsReturnCode.EBICS_OK) && 
(!isEbicsCodeTolerated)) {
+        tech.libeufin.nexus.logger.error("EBICS return code is 
${respObj.technicalReturnCode}, failing.")
+        return null
+    }
+    var isBankCodeTolerated = false
+    if (tolerateBankReturnCode != null)
+        isBankCodeTolerated = respObj.bankReturnCode == tolerateBankReturnCode
+
+    // Business error, although EBICS itself was correct.
+    if ((respObj.bankReturnCode != EbicsReturnCode.EBICS_OK) && 
(!isBankCodeTolerated)) {
+        tech.libeufin.nexus.logger.error("Bank-technical return code is 
${respObj.technicalReturnCode}, failing.")
+        return null
+    }
+    return respObj
+}
+/**
+ * Collects all the steps of an EBICS download transaction.  Namely
+ * it conducts: init -> transfer -> receipt phases.
+ *
+ * @param client HTTP client for POSTing to the bank.
+ * @param cfg configuration handle.
+ * @param clientKeys client EBICS private keys.
+ * @param bankKeys bank EBICS public keys.
+ * @param reqXml raw EBICS XML request.
+ * @return the bank response as an XML string, or null if one
+ *         error took place.  NOTE: any return code other than
+ *         EBICS_OK constitutes an error.
+ */
+suspend fun doEbicsDownload(
+    client: HttpClient,
+    cfg: EbicsSetupConfig,
+    clientKeys: ClientPrivateKeysFile,
+    bankKeys: BankPublicKeysFile,
+    reqXml: String,
+    isEbics3: Boolean
+): String? {
+    val initResp = postEbicsAndCheckReturnCodes(client, cfg, bankKeys, reqXml, 
isEbics3)
+    if (initResp == null) {
+        tech.libeufin.nexus.logger.error("Could not get past the EBICS init 
phase, failing.")
+        return null
+    }
+    val howManySegments = initResp.numSegments
+    if (howManySegments == null) {
+        tech.libeufin.nexus.logger.error("Init response lacks the quantity of 
segments, failing.")
+        return null
+    }
+    val ebicsChunks = mutableListOf<String>()
+    // Getting the chunk(s)
+    val firstDataChunk = initResp.orderDataEncChunk
+    if (firstDataChunk == null) {
+        tech.libeufin.nexus.logger.error("Could not get the first data chunk, 
although the EBICS_OK return code, failing.")
+        return null
+    }
+    val dataEncryptionInfo = initResp.dataEncryptionInfo ?: run {
+        tech.libeufin.nexus.logger.error("EncryptionInfo element not found, 
despite non empty payload, failing.")
+        return null
+    }
+    ebicsChunks.add(firstDataChunk)
+    val tId = initResp.transactionID
+    if (tId == null) {
+        tech.libeufin.nexus.logger.error("Transaction ID not found in the init 
response, cannot do transfer phase, failing.")
+        return null
+    }
+    // proceed with the transfer phase.
+    for (x in 2 .. howManySegments) {
+        // request segment number x.
+        val transReq = createEbics25TransferPhase(cfg, clientKeys, x, 
howManySegments, tId)
+        val transResp = postEbicsAndCheckReturnCodes(client, cfg, bankKeys, 
transReq, isEbics3)
+        if (transResp == null) {
+            tech.libeufin.nexus.logger.error("EBICS transfer segment #$x 
failed.")
+            return null
+        }
+        val chunk = transResp.orderDataEncChunk
+        if (chunk == null) {
+            tech.libeufin.nexus.logger.error("EBICS transfer phase lacks chunk 
#$x, failing.")
+            return null
+        }
+        ebicsChunks.add(chunk)
+    }
+    // all chunks gotten, shaping a meaningful response now.
+    val payloadBytes = decryptAndDecompressPayload(
+        clientKeys.encryption_private_key,
+        dataEncryptionInfo,
+        ebicsChunks
+    )
+    // payload reconstructed, ack to the bank.
+    val ackXml = createEbics25ReceiptPhase(cfg, clientKeys, tId)
+    val ackResp = postEbicsAndCheckReturnCodes(
+        client,
+        cfg,
+        bankKeys,
+        ackXml,
+        isEbics3,
+        tolerateEbicsReturnCode = 
EbicsReturnCode.EBICS_DOWNLOAD_POSTPROCESS_DONE
+    )
+    if (ackResp == null) {
+        tech.libeufin.nexus.logger.error("EBICS receipt phase failed.")
+        return null
+    }
+    // receipt phase OK, can now return the payload as an XML string.
+    return try {
+        payloadBytes.toString(Charsets.UTF_8)
+    } catch (e: Exception) {
+        logger.error("Could not get the XML string out of payload bytes.")
+        null
+    }
+}
+
+/**
+ * Parses the bank response from the raw XML and verifies
+ * the bank signature.
+ *
+ * @param bankKeys provides the bank auth pub, to verify the signature.
+ * @param responseStr raw XML response from the bank
+ * @param withEbics3 true if the communication is EBICS 3, false otherwise.
+ * @return libeufin internal representation of EBICS responses.  Null
+ *         in case of errors.
+ */
+fun parseAndValidateEbicsResponse(
+    bankKeys: BankPublicKeysFile,
+    responseStr: String,
+    withEbics3: Boolean
+): EbicsResponseContent? {
+    val responseDocument = try {
+        XMLUtil.parseStringIntoDom(responseStr)
+    } catch (e: Exception) {
+        tech.libeufin.nexus.logger.error("Bank response apparently invalid.")
+        return null
+    }
+    if (!XMLUtil.verifyEbicsDocument(
+            responseDocument,
+            bankKeys.bank_authentication_public_key,
+            withEbics3
+        )) {
+        tech.libeufin.nexus.logger.error("Bank signature did not verify.")
+        return null
+    }
+    if (withEbics3)
+        return ebics3toInternalRepr(responseStr)
+    return ebics25toInternalRepr(responseStr)
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
new file mode 100644
index 00000000..361bdd95
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
@@ -0,0 +1,261 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 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/>
+ */
+
+/**
+ * This file contains helpers to construct EBICS 2.x requests.
+ */
+
+package tech.libeufin.nexus.ebics
+
+import io.ktor.client.*
+import tech.libeufin.nexus.BankPublicKeysFile
+import tech.libeufin.nexus.ClientPrivateKeysFile
+import tech.libeufin.nexus.EbicsSetupConfig
+import tech.libeufin.util.*
+import tech.libeufin.util.ebics_h004.*
+import java.security.interfaces.RSAPrivateCrtKey
+import java.time.ZoneId
+import java.util.*
+import javax.xml.datatype.DatatypeFactory
+
+/**
+ * Request EBICS (2.x) HTD to the bank.  This message type
+ * gets the list of bank accounts that are owned by the EBICS
+ * client.
+ *
+ * @param cfg configuration handle
+ * @param client client EBICS keys.
+ * @param bankKeys bank EBICS keys.
+ * @param client HTTP client handle.
+ * @return internal representation of the HTD response, or
+ *         null in case of errors.
+ */
+suspend fun fetchBankAccounts(
+    cfg: EbicsSetupConfig,
+    clientKeys: ClientPrivateKeysFile,
+    bankKeys: BankPublicKeysFile,
+    client: HttpClient
+): HTDResponseOrderData? {
+    val xmlReq = createEbics25DownloadInit(cfg, clientKeys, bankKeys, "HTD")
+    val xmlResp = doEbicsDownload(client, cfg, clientKeys, bankKeys, xmlReq, 
false)
+    if (xmlResp == null) {
+        logger.error("EBICS HTD transaction failed.")
+        return null
+    }
+    return try {
+        XMLUtil.convertStringToJaxb<HTDResponseOrderData>(xmlResp).value
+    } catch (e: Exception) {
+        logger.error("Could not parse the HTD payload, detail: ${e.message}")
+        return null
+    }
+}
+/**
+ * Creates a EBICS 2.5 download init. message.  So far only used
+ * to fetch the PostFinance bank accounts.
+ */
+fun createEbics25DownloadInit(
+    cfg: EbicsSetupConfig,
+    clientKeys: ClientPrivateKeysFile,
+    bankKeys: BankPublicKeysFile,
+    orderType: String,
+    orderParams: EbicsOrderParams = EbicsStandardOrderParams()
+): String {
+    val nonce = getNonce(128)
+    val req = EbicsRequest.createForDownloadInitializationPhase(
+        cfg.ebicsUserId,
+        cfg.ebicsPartnerId,
+        cfg.ebicsHostId,
+        nonce,
+        DatatypeFactory.newInstance().newXMLGregorianCalendar(
+            GregorianCalendar(
+            TimeZone.getTimeZone(ZoneId.systemDefault())
+        )
+        ),
+        bankKeys.bank_encryption_public_key,
+        bankKeys.bank_authentication_public_key,
+        orderType,
+        makeOrderParams(orderParams)
+    )
+    val doc = XMLUtil.convertJaxbToDocument(req)
+    XMLUtil.signEbicsDocument(
+        doc,
+        clientKeys.authentication_private_key,
+        withEbics3 = false
+    )
+    return XMLUtil.convertDomToString(doc)
+}
+
+/**
+ * Creates raw XML for an EBICS receipt phase.
+ *
+ * @param cfg configuration handle.
+ * @param clientKeys user EBICS private keys.
+ * @param transactionId transaction ID of the EBICS communication that
+ *        should receive this receipt.
+ * @return receipt request in XML.
+ */
+fun createEbics25ReceiptPhase(
+    cfg: EbicsSetupConfig,
+    clientKeys: ClientPrivateKeysFile,
+    transactionId: String
+): String {
+    val req = EbicsRequest.createForDownloadReceiptPhase(
+        transactionId,
+        cfg.ebicsHostId
+    )
+    val doc = XMLUtil.convertJaxbToDocument(req)
+    XMLUtil.signEbicsDocument(
+        doc,
+        clientKeys.authentication_private_key,
+        withEbics3 = false
+    )
+    return XMLUtil.convertDomToString(doc)
+}
+
+/**
+ * Creates raw XML for an EBICS transfer phase.
+ *
+ * @param cfg configuration handle.
+ * @param clientKeys user EBICS private keys.
+ * @param segNumber which segment we ask to the bank.
+ * @param totalSegments how many segments compose the whole EBICS transaction.
+ * @param transactionId ID of the EBICS transaction that transports all the 
segments.
+ * @return raw XML string of the request.
+ */
+fun createEbics25TransferPhase(
+    cfg: EbicsSetupConfig,
+    clientKeys: ClientPrivateKeysFile,
+    segNumber: Int,
+    totalSegments: Int,
+    transactionId: String
+): String {
+    val req = EbicsRequest.createForDownloadTransferPhase(
+        hostID = cfg.ebicsHostId,
+        segmentNumber = segNumber,
+        numSegments = totalSegments,
+        transactionID = transactionId
+    )
+    val doc = XMLUtil.convertJaxbToDocument(req)
+    XMLUtil.signEbicsDocument(
+        doc,
+        clientKeys.authentication_private_key,
+        withEbics3 = false
+    )
+    return XMLUtil.convertDomToString(doc)
+}
+
+/**
+ * Parses the raw XML that came from the bank into the Nexus representation.
+ *
+ * @param clientEncryptionKey client private encryption key, used to decrypt
+ *                            the transaction key.
+ * @param xml the bank raw XML response
+ * @return the internal representation of the XML response, or null if the 
parsing or the decryption failed.
+ *         Note: it _is_ possible to successfully return the internal repr. of 
this response, where
+ *         the payload is null.  That's however still useful, because the 
returned type provides bank
+ *         and EBICS return codes.
+ */
+fun parseKeysMgmtResponse(
+    clientEncryptionKey: RSAPrivateCrtKey,
+    xml: String
+): EbicsKeyManagementResponseContent? {
+    val jaxb = try {
+        XMLUtil.convertStringToJaxb<EbicsKeyManagementResponse>(xml)
+    } catch (e: Exception) {
+        tech.libeufin.nexus.logger.error("Could not parse the raw response 
from bank into JAXB.")
+        return null
+    }
+    var payload: ByteArray? = null
+    jaxb.value.body.dataTransfer?.dataEncryptionInfo.apply {
+        // non-null indicates that an encrypted payload should be found.
+        if (this != null) {
+            val encOrderData = jaxb.value.body.dataTransfer?.orderData?.value
+            if (encOrderData == null) {
+                tech.libeufin.nexus.logger.error("Despite a non-null 
DataEncryptionInfo, OrderData could not be found, can't decrypt any payload!")
+                return null
+            }
+            payload = decryptAndDecompressPayload(
+                clientEncryptionKey,
+                DataEncryptionInfo(this.transactionKey, 
this.encryptionPubKeyDigest.value),
+                listOf(encOrderData)
+            )
+        }
+    }
+    val bankReturnCode = 
EbicsReturnCode.lookup(jaxb.value.body.returnCode.value) // business error
+    val ebicsReturnCode = 
EbicsReturnCode.lookup(jaxb.value.header.mutable.returnCode) // ebics error
+    return EbicsKeyManagementResponseContent(ebicsReturnCode, bankReturnCode, 
payload)
+}
+
+/**
+ * Generates the INI message to upload the signature key.
+ *
+ * @param cfg handle to the configuration.
+ * @param clientKeys set of all the client keys.
+ * @return the raw EBICS INI message.
+ */
+fun generateIniMessage(cfg: EbicsSetupConfig, clientKeys: 
ClientPrivateKeysFile): String {
+    val iniRequest = EbicsUnsecuredRequest.createIni(
+        cfg.ebicsHostId,
+        cfg.ebicsUserId,
+        cfg.ebicsPartnerId,
+        clientKeys.signature_private_key
+    )
+    val doc = XMLUtil.convertJaxbToDocument(iniRequest)
+    return XMLUtil.convertDomToString(doc)
+}
+
+/**
+ * Generates the HIA message: uploads the authentication and
+ * encryption keys.
+ *
+ * @param cfg handle to the configuration.
+ * @param clientKeys set of all the client keys.
+ * @return the raw EBICS HIA message.
+ */
+fun generateHiaMessage(cfg: EbicsSetupConfig, clientKeys: 
ClientPrivateKeysFile): String {
+    val hiaRequest = EbicsUnsecuredRequest.createHia(
+        cfg.ebicsHostId,
+        cfg.ebicsUserId,
+        cfg.ebicsPartnerId,
+        clientKeys.authentication_private_key,
+        clientKeys.encryption_private_key
+    )
+    val doc = XMLUtil.convertJaxbToDocument(hiaRequest)
+    return XMLUtil.convertDomToString(doc)
+}
+
+/**
+ * Generates the HPB message: downloads the bank keys.
+ *
+ * @param cfg handle to the configuration.
+ * @param clientKeys set of all the client keys.
+ * @return the raw EBICS HPB message.
+ */
+fun generateHpbMessage(cfg: EbicsSetupConfig, clientKeys: 
ClientPrivateKeysFile): String {
+    val hpbRequest = EbicsNpkdRequest.createRequest(
+        cfg.ebicsHostId,
+        cfg.ebicsPartnerId,
+        cfg.ebicsUserId,
+        getNonce(128),
+        
DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar())
+    )
+    val doc = XMLUtil.convertJaxbToDocument(hpbRequest)
+    XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key)
+    return XMLUtil.convertDomToString(doc)
+}
\ No newline at end of file
diff --git a/nexus/src/main/resources/logback.xml 
b/nexus/src/main/resources/logback.xml
new file mode 100644
index 00000000..b18b437e
--- /dev/null
+++ b/nexus/src/main/resources/logback.xml
@@ -0,0 +1,23 @@
+<!-- configuration scan="true" -->
+<configuration>
+    <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
+       <target>System.err</target>
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - 
%msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="tech.libeufin.nexus" level="ALL"  additivity="false">
+        <appender-ref ref="STDERR" />
+    </logger>
+
+    <logger name="io.netty" level="WARN"/>
+    <logger name="ktor" level="WARN"/>
+    <logger name="Exposed" level="WARN"/>
+    <logger name="tech.libeufin.util" level="DEBUG"/>
+
+    <root level="WARN">
+        <appender-ref ref="STDERR"/>
+    </root>
+
+</configuration>
diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt
new file mode 100644
index 00000000..b9284822
--- /dev/null
+++ b/nexus/src/test/kotlin/Common.kt
@@ -0,0 +1,50 @@
+import io.ktor.client.*
+import io.ktor.client.engine.mock.*
+import io.ktor.client.request.*
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import tech.libeufin.nexus.*
+import java.security.interfaces.RSAPrivateCrtKey
+
+val j = Json {
+    this.serializersModule = SerializersModule {
+        contextual(RSAPrivateCrtKey::class) { RSAPrivateCrtKeySerializer }
+    }
+}
+
+val config: EbicsSetupConfig = run {
+    val handle = TalerConfig(NEXUS_CONFIG_SOURCE)
+    handle.load()
+    EbicsSetupConfig(handle)
+}
+
+val clientKeys = generateNewKeys()
+
+// Gets an HTTP client whose requests are going to be served by 'handler'.
+fun getMockedClient(
+    handler: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
+): HttpClient {
+    return HttpClient(MockEngine) {
+        followRedirects = false
+        engine {
+            addHandler {
+                    request -> handler(request)
+            }
+        }
+    }
+}
+
+fun getPofiConfig(userId: String, partnerId: String) = """
+    [nexus-ebics]
+    CURRENCY = KUDOS
+    HOST_BASE_URL = https://isotest.postfinance.ch/ebicsweb/ebicsweb
+    HOST_ID = PFEBICS
+    USER_ID = $userId
+    PARTNER_ID = $partnerId
+    SYSTEM_ID = not-used
+    ACCOUNT_NUMBER = not-used-yet
+    BANK_PUBLIC_KEYS_FILE = /tmp/enc-auth-keys.json
+    CLIENT_PRIVATE_KEYS_FILE = /tmp/my-private-keys.json
+    ACCOUNT_META_DATA_FILE = /tmp/ebics-meta.json
+    BANK_DIALECT = postfinance
+""".trimIndent()
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/ConfigLoading.kt 
b/nexus/src/test/kotlin/ConfigLoading.kt
new file mode 100644
index 00000000..1216a13a
--- /dev/null
+++ b/nexus/src/test/kotlin/ConfigLoading.kt
@@ -0,0 +1,35 @@
+import org.junit.Test
+import org.junit.jupiter.api.assertThrows
+import tech.libeufin.nexus.EbicsSetupConfig
+import tech.libeufin.nexus.NEXUS_CONFIG_SOURCE
+
+class ConfigLoading {
+    /**
+     * Tests that the default configuration has _at least_ the options
+     * that are expected by the memory representation of config.
+     */
+    @Test
+    fun loadRequiredValues() {
+        val handle = TalerConfig(NEXUS_CONFIG_SOURCE)
+        handle.load()
+        val cfg = EbicsSetupConfig(handle)
+        cfg._dump()
+    }
+
+    /**
+     * Tests that if the configuration lacks at least one option, then
+     * the config loader throws exception.
+     */
+    @Test
+    fun detectMissingValues() {
+        val handle = TalerConfig(NEXUS_CONFIG_SOURCE)
+        handle.loadFromString("""
+            [ebics-nexus]
+            # All the other defaults won't be loaded.
+            BANK_DIALECT = postfinance
+        """.trimIndent())
+        assertThrows<TalerConfigError> {
+            EbicsSetupConfig(handle)
+        }
+    }
+}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/Ebics.kt b/nexus/src/test/kotlin/Ebics.kt
new file mode 100644
index 00000000..5fd7e32a
--- /dev/null
+++ b/nexus/src/test/kotlin/Ebics.kt
@@ -0,0 +1,122 @@
+import io.ktor.client.*
+import io.ktor.client.engine.mock.*
+import io.ktor.http.*
+import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
+import org.junit.Test
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.ebics.*
+import tech.libeufin.util.XMLUtil
+import tech.libeufin.util.ebics_h004.EbicsUnsecuredRequest
+import java.io.File
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class Ebics {
+
+    // Checks XML is valid and INI.
+    @Test
+    fun iniMessage() {
+        val msg = generateIniMessage(config, clientKeys)
+        val ini = XMLUtil.convertStringToJaxb<EbicsUnsecuredRequest>(msg) // 
ensures is valid
+        assertEquals(ini.value.header.static.orderDetails.orderType, "INI") // 
ensures is INI
+    }
+
+    // Checks XML is valid and HIA.
+    @Test
+    fun hiaMessage() {
+        val msg = generateHiaMessage(config, clientKeys)
+        val ini = XMLUtil.convertStringToJaxb<EbicsUnsecuredRequest>(msg) // 
ensures is valid
+        assertEquals(ini.value.header.static.orderDetails.orderType, "HIA") // 
ensures is HIA
+    }
+
+    // Checks XML is valid and HPB.
+    @Test
+    fun hpbMessage() {
+        val msg = generateHpbMessage(config, clientKeys)
+        val ini = XMLUtil.convertStringToJaxb<EbicsUnsecuredRequest>(msg) // 
ensures is valid
+        assertEquals(ini.value.header.static.orderDetails.orderType, "HPB") // 
ensures is HPB
+    }
+    // POSTs an EBICS message to the mock bank.  Tests
+    // the main branches: unreachable bank, non-200 status
+    // code, and 200.
+    @Test
+    fun postMessage() {
+        val client404 = getMockedClient {
+            respondError(HttpStatusCode.NotFound)
+        }
+        val clientNoResponse = getMockedClient {
+            throw Exception("Network issue.")
+        }
+        val clientOk = getMockedClient {
+            respondOk("Not EBICS anyway.")
+        }
+        runBlocking {
+            assertNull(client404.postToBank("http://ignored.example.com/";, 
"ignored"))
+            
assertNull(clientNoResponse.postToBank("http://ignored.example.com/";, 
"ignored"))
+            assertNotNull(clientOk.postToBank("http://ignored.example.com/";, 
"ignored"))
+        }
+    }
+
+    // Tests that internal repr. of keys lead to valid PDF.
+    // Mainly tests that the function does not throw any error.
+    @Test
+    fun keysPdf() {
+        val pdf = generateKeysPdf(clientKeys, config)
+        File("/tmp/libeufin-nexus-test-keys.pdf").writeBytes(pdf)
+    }
+}
+
+@Ignore // manual tests
+class PostFinance {
+    private fun prep(): EbicsSetupConfig {
+        val handle = TalerConfig(NEXUS_CONFIG_SOURCE)
+        val ebicsUserId = File("/tmp/pofi-ebics-user-id.txt").readText()
+        val ebicsPartnerId = File("/tmp/pofi-ebics-partner-id.txt").readText()
+        handle.loadFromString(getPofiConfig(ebicsUserId, ebicsPartnerId))
+        return EbicsSetupConfig(handle)
+    }
+    // Tests sending client keys to the PostFinance test platform.
+    @Test
+    fun postClientKeys() {
+        val cfg = prep()
+        runBlocking {
+            val httpClient = HttpClient()
+            assertTrue(doKeysRequestAndUpdateState(cfg, clientKeys, 
httpClient, KeysOrderType.INI))
+            assertTrue(doKeysRequestAndUpdateState(cfg, clientKeys, 
httpClient, KeysOrderType.HIA))
+        }
+    }
+
+    // Tests getting the PostFinance keys from their test platform.
+    @Test
+    fun getBankKeys() {
+        val cfg = prep()
+        val keys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
+        assertNotNull(keys)
+        assertTrue(keys.submitted_ini)
+        assertTrue(keys.submitted_hia)
+        runBlocking {
+            assertTrue(doKeysRequestAndUpdateState(
+                cfg,
+                keys,
+                HttpClient(),
+                KeysOrderType.HPB
+            ))
+        }
+    }
+
+    // Tests the HTD message type.
+    @Test
+    fun fetchAccounts() {
+        val cfg = prep()
+        val clientKeys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
+        assertNotNull(clientKeys)
+        val bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)
+        assertNotNull(bankKeys)
+        val htd = runBlocking { fetchBankAccounts(cfg, clientKeys, bankKeys, 
HttpClient()) }
+        assertNotNull(htd)
+        println(htd.partnerInfo.accountInfoList?.size)
+    }
+}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/Keys.kt b/nexus/src/test/kotlin/Keys.kt
new file mode 100644
index 00000000..db598d44
--- /dev/null
+++ b/nexus/src/test/kotlin/Keys.kt
@@ -0,0 +1,88 @@
+import org.junit.Test
+import tech.libeufin.nexus.*
+import tech.libeufin.util.CryptoUtil
+import java.io.File
+import kotlin.test.*
+
+class PublicKeys {
+
+    // Tests intermittent spaces in public keys fingerprint.
+    @Test
+    fun splitTest() {
+        assertEquals("0099887766".spaceEachTwo(), "00 99 88 77 66") // even
+        assertEquals("ZZYYXXWWVVU".spaceEachTwo(), "ZZ YY XX WW VV U") // odd
+    }
+
+    // Tests loading the bank public keys from disk.
+    @Test
+    fun loadBankKeys() {
+        // artificially creating the keys.
+        val fileContent = BankPublicKeysFile(
+            accepted = true,
+            bank_authentication_public_key = 
CryptoUtil.generateRsaKeyPair(2028).public,
+            bank_encryption_public_key = 
CryptoUtil.generateRsaKeyPair(2028).public
+        )
+        // storing them on disk.
+        assertTrue(syncJsonToDisk(fileContent, config.bankPublicKeysFilename))
+        // loading them and check that values are the same.
+        val fromDisk = loadBankKeys(config.bankPublicKeysFilename)
+        assertNotNull(fromDisk)
+        assertTrue {
+            fromDisk.accepted &&
+                    fromDisk.bank_encryption_public_key == 
fileContent.bank_encryption_public_key &&
+                    fromDisk.bank_authentication_public_key == 
fileContent.bank_authentication_public_key
+        }
+    }
+    @Test
+    fun loadNotFound() {
+        assertNull(loadBankKeys("/tmp/highly-unlikely-to-be-found.json"))
+    }
+}
+class PrivateKeys {
+    val f = File("/tmp/nexus-privs-test.json")
+    init {
+        if (f.exists())
+            f.delete()
+    }
+
+    // Testing write failure due to insufficient permissions.
+    @Test
+    fun createWrongPermissions() {
+        f.writeText("won't be overridden")
+        f.setReadOnly()
+        assertFalse(syncJsonToDisk(clientKeys, f.path))
+    }
+
+    // Testing keys file creation.
+    @Test
+    fun creation() {
+        assertFalse(f.exists())
+        maybeCreatePrivateKeysFile(f.path) // file doesn't exist, this must 
create.
+        j.decodeFromString<ClientPrivateKeysFile>(f.readText()) // reading and 
validating disk content.
+    }
+    /**
+     * Tests whether loading keys from disk yields the same
+     * values that were stored to the file.
+     */
+    @Test
+    fun load() {
+        assertFalse(f.exists())
+        assertTrue(syncJsonToDisk(clientKeys, f.path)) // Artificially storing 
this to the file.
+        val fromDisk = loadPrivateKeysFromDisk(f.path) // loading it via the 
tested routine.
+        assertNotNull(fromDisk)
+        // Checking the values from disk match the initial object.
+        assertTrue {
+            clientKeys.authentication_private_key == 
fromDisk.authentication_private_key &&
+                    clientKeys.encryption_private_key == 
fromDisk.encryption_private_key &&
+                    clientKeys.signature_private_key == 
fromDisk.signature_private_key &&
+                    clientKeys.submitted_ini == fromDisk.submitted_ini &&
+                    clientKeys.submitted_hia == fromDisk.submitted_hia
+        }
+    }
+
+    // Testing failure on file not found.
+    @Test
+    fun loadNotFound() {
+        
assertNull(loadPrivateKeysFromDisk("/tmp/highly-unlikely-to-be-found.json"))
+    }
+}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/MySerializers.kt 
b/nexus/src/test/kotlin/MySerializers.kt
new file mode 100644
index 00000000..75dfb46f
--- /dev/null
+++ b/nexus/src/test/kotlin/MySerializers.kt
@@ -0,0 +1,32 @@
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import net.taler.wallet.crypto.Base32Crockford
+import org.junit.Test
+import tech.libeufin.nexus.ClientPrivateKeysFile
+import tech.libeufin.nexus.RSAPrivateCrtKeySerializer
+import tech.libeufin.util.CryptoUtil
+import java.security.interfaces.RSAPrivateCrtKey
+import kotlin.test.assertEquals
+
+class MySerializers {
+    // Testing deserialization of RSA private keys.
+    @Test
+    fun rsaPrivDeserialization() {
+        val s = 
Base32Crockford.encode(CryptoUtil.generateRsaKeyPair(2048).private.encoded)
+        val a = 
Base32Crockford.encode(CryptoUtil.generateRsaKeyPair(2048).private.encoded)
+        val e = 
Base32Crockford.encode(CryptoUtil.generateRsaKeyPair(2048).private.encoded)
+        val obj = j.decodeFromString<ClientPrivateKeysFile>("""
+            {
+              "signature_private_key": "$s",
+              "authentication_private_key": "$a",
+              "encryption_private_key": "$e",
+              "submitted_ini": true,
+              "submitted_hia": true
+            }
+        """.trimIndent())
+        assertEquals(obj.signature_private_key, 
CryptoUtil.loadRsaPrivateKey(Base32Crockford.decode(s)))
+        assertEquals(obj.authentication_private_key, 
CryptoUtil.loadRsaPrivateKey(Base32Crockford.decode(a)))
+        assertEquals(obj.encryption_private_key, 
CryptoUtil.loadRsaPrivateKey(Base32Crockford.decode(e)))
+    }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 0cccd975..8c5fe595 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,4 @@
 rootProject.name = 'libeufin'
 include("bank")
-// include("nexus")
+include("nexus")
 include("util")
diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt
index 837f49ba..80585f1c 100644
--- a/util/src/main/kotlin/Ebics.kt
+++ b/util/src/main/kotlin/Ebics.kt
@@ -99,7 +99,7 @@ data class EbicsClientSubscriberDetails(
 /**
  * @param size in bits
  */
-private fun getNonce(size: Int): ByteArray {
+fun getNonce(size: Int): ByteArray {
     val sr = SecureRandom()
     val ret = ByteArray(size / 8)
     sr.nextBytes(ret)
@@ -120,7 +120,7 @@ private fun getXmlDate(d: ZonedDateTime): 
XMLGregorianCalendar {
         )
 }
 
-private fun makeOrderParams(orderParams: EbicsOrderParams): 
EbicsRequest.OrderParams {
+fun makeOrderParams(orderParams: EbicsOrderParams): EbicsRequest.OrderParams {
     return when (orderParams) {
         is EbicsStandardOrderParams -> {
             EbicsRequest.StandardOrderParams().apply {
@@ -637,7 +637,7 @@ fun parseEbicsHpbOrder(orderDataRaw: ByteArray): 
HpbResponseData {
     )
 }
 
-private fun ebics3toInternalRepr(response: String): EbicsResponseContent {
+fun ebics3toInternalRepr(response: String): EbicsResponseContent {
     // logger.debug("Converting bank resp to internal repr.: $response")
     val resp: JAXBElement<Ebics3Response> = try {
         XMLUtil.convertStringToJaxb(response)
@@ -674,7 +674,7 @@ private fun ebics3toInternalRepr(response: String): 
EbicsResponseContent {
     )
 }
 
-private fun ebics25toInternalRepr(response: String): EbicsResponseContent {
+fun ebics25toInternalRepr(response: String): EbicsResponseContent {
     val resp: JAXBElement<EbicsResponse> = try {
         XMLUtil.convertStringToJaxb(response)
     } catch (e: Exception) {

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]