summaryrefslogtreecommitdiff
path: root/nexus/src
diff options
context:
space:
mode:
Diffstat (limited to 'nexus/src')
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt13
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt1
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt1
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt330
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt35
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt209
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt204
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt288
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt2
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt6
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt1
-rw-r--r--nexus/src/test/kotlin/Ebics.kt2
-rw-r--r--nexus/src/test/kotlin/XmlCombinatorsTest.kt76
-rw-r--r--nexus/src/test/kotlin/XmlUtilTest.kt73
-rw-r--r--nexus/src/test/resources/signature1/doc.xml9
-rw-r--r--nexus/src/test/resources/signature1/public_key.txt1
16 files changed, 1234 insertions, 17 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index d6ae13f6..a089012a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -27,7 +27,6 @@ import io.ktor.client.*
import io.ktor.client.plugins.*
import kotlinx.coroutines.*
import tech.libeufin.common.*
-import tech.libeufin.ebics.*
import tech.libeufin.nexus.ebics.*
import java.io.IOException
import java.io.InputStream
@@ -307,7 +306,7 @@ private fun ingestDocuments(
private suspend fun fetchDocuments(
db: Database,
ctx: FetchContext,
- docs: List<Document>
+ docs: List<EbicsDocument>
): Boolean {
val lastExecutionTime: Instant? = ctx.pinnedStart
return docs.all { doc ->
@@ -334,7 +333,7 @@ private suspend fun fetchDocuments(
}
}
-enum class Document {
+enum class EbicsDocument {
/// EBICS acknowledgement - CustomerAcknowledgement HAC pain.002
acknowledgement,
/// Payment status - CustomerPaymentStatusReport pain.002
@@ -379,10 +378,10 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") {
help = "This flag fetches only once from the bank and returns, " +
"ignoring the 'frequency' configuration value"
).flag(default = false)
- private val documents: Set<Document> by argument(
+ private val documents: Set<EbicsDocument> by argument(
help = "Which documents should be fetched? If none are specified, all supported documents will be fetched",
- helpTags = Document.entries.map { Pair(it.name, it.shortDescription()) }.toMap()
- ).enum<Document>().multiple().unique()
+ helpTags = EbicsDocument.entries.map { Pair(it.name, it.shortDescription()) }.toMap()
+ ).enum<EbicsDocument>().multiple().unique()
private val pinnedStart by option(
help = "Constant YYYY-MM-DD date for the earliest document" +
" to download (only consumed in --transient mode). The" +
@@ -419,7 +418,7 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") {
null,
FileLogger(ebicsLog)
)
- val docs = if (documents.isEmpty()) Document.entries else documents.toList()
+ val docs = if (documents.isEmpty()) EbicsDocument.entries else documents.toList()
if (transient) {
logger.info("Transient mode: fetching once and returning.")
val pinnedStartVal = pinnedStart
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
index d0e1a7f3..58e43e2d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
@@ -26,7 +26,6 @@ import io.ktor.client.*
import io.ktor.client.plugins.*
import tech.libeufin.common.*
import tech.libeufin.common.crypto.*
-import tech.libeufin.ebics.*
import tech.libeufin.nexus.ebics.*
import java.nio.file.*
import java.time.Instant
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
index 946a1808..e2b673dd 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -19,7 +19,6 @@
package tech.libeufin.nexus
import tech.libeufin.common.*
-import tech.libeufin.ebics.*
import java.io.InputStream
import java.net.URLEncoder
import java.time.*
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt
new file mode 100644
index 00000000..bc2c7eae
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt
@@ -0,0 +1,330 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * 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 IS GENERATED, DO NOT EDIT
+
+package tech.libeufin.nexus
+
+enum class ExternalStatusReasonCode(val isoCode: String, val description: String) {
+ AB01("AbortedClearingTimeout", "Clearing process aborted due to timeout."),
+ AB02("AbortedClearingFatalError", "Clearing process aborted due to a fatal error."),
+ AB03("AbortedSettlementTimeout", "Settlement aborted due to timeout."),
+ AB04("AbortedSettlementFatalError", "Settlement process aborted due to a fatal error."),
+ AB05("TimeoutCreditorAgent", "Transaction stopped due to timeout at the Creditor Agent."),
+ AB06("TimeoutInstructedAgent", "Transaction stopped due to timeout at the Instructed Agent."),
+ AB07("OfflineAgent", "Agent of message is not online."),
+ AB08("OfflineCreditorAgent", "Creditor Agent is not online."),
+ AB09("ErrorCreditorAgent", "Transaction stopped due to error at the Creditor Agent."),
+ AB10("ErrorInstructedAgent", "Transaction stopped due to error at the Instructed Agent."),
+ AB11("TimeoutDebtorAgent", "Transaction stopped due to timeout at the Debtor Agent."),
+ AC01("IncorrectAccountNumber", "Account number is invalid or missing."),
+ AC02("InvalidDebtorAccountNumber", "Debtor account number invalid or missing"),
+ AC03("InvalidCreditorAccountNumber", "Creditor account number invalid or missing"),
+ AC04("ClosedAccountNumber", "Account number specified has been closed on the bank of account's books."),
+ AC05("ClosedDebtorAccountNumber", "Debtor account number closed"),
+ AC06("BlockedAccount", "Account specified is blocked, prohibiting posting of transactions against it."),
+ AC07("ClosedCreditorAccountNumber", "Creditor account number closed"),
+ AC08("InvalidBranchCode", "Branch code is invalid or missing"),
+ AC09("InvalidAccountCurrency", "Account currency is invalid or missing"),
+ AC10("InvalidDebtorAccountCurrency", "Debtor account currency is invalid or missing"),
+ AC11("InvalidCreditorAccountCurrency", "Creditor account currency is invalid or missing"),
+ AC12("InvalidAccountType", "Account type missing or invalid."),
+ AC13("InvalidDebtorAccountType", "Debtor account type missing or invalid"),
+ AC14("InvalidCreditorAccountType", "Creditor account type missing or invalid"),
+ AC15("AccountDetailsChanged", "The account details for the counterparty have changed."),
+ AC16("CardNumberInvalid", "Credit or debit card number is invalid."),
+ AEXR("AlreadyExpiredRTP", "Request-to-pay Expiry Date and Time has already passed."),
+ AG01("TransactionForbidden", "Transaction forbidden on this type of account (formerly NoAgreement)"),
+ AG02("InvalidBankOperationCode", "Bank Operation code specified in the message is not valid for receiver"),
+ AG03("TransactionNotSupported", "Transaction type not supported/authorized on this account"),
+ AG04("InvalidAgentCountry", "Agent country code is missing or invalid."),
+ AG05("InvalidDebtorAgentCountry", "Debtor agent country code is missing or invalid"),
+ AG06("InvalidCreditorAgentCountry", "Creditor agent country code is missing or invalid"),
+ AG07("UnsuccesfulDirectDebit", "Debtor account cannot be debited for a generic reason."),
+ AG08("InvalidAccessRights", "Transaction failed due to invalid or missing user or access right"),
+ AG09("PaymentNotReceived", "Original payment never received."),
+ AG10("AgentSuspended", "Agent of message is suspended from the Real Time Payment system."),
+ AG11("CreditorAgentSuspended", "Creditor Agent of message is suspended from the Real Time Payment system."),
+ AG12("NotAllowedBookTransfer", "Payment orders made by transferring funds from one account to another at the same financial institution (bank or payment institution) are not allowed."),
+ AG13("ForbiddenReturnPayment", "Returned payments derived from previously returned transactions are not allowed."),
+ AGNT("IncorrectAgent", "Agent in the payment workflow is incorrect"),
+ ALAC("AlreadyAcceptedRTP", "Request-to-pay has already been accepted by the Debtor."),
+ AM01("ZeroAmount", "Specified message amount is equal to zero"),
+ AM02("NotAllowedAmount", "Specific transaction/message amount is greater than allowed maximum"),
+ AM03("NotAllowedCurrency", "Specified message amount is an non processable currency outside of existing agreement"),
+ AM04("InsufficientFunds", "Amount of funds available to cover specified message amount is insufficient."),
+ AM05("Duplication", "Duplication"),
+ AM06("TooLowAmount", "Specified transaction amount is less than agreed minimum."),
+ AM07("BlockedAmount", "Amount specified in message has been blocked by regulatory authorities."),
+ AM09("WrongAmount", "Amount received is not the amount agreed or expected"),
+ AM10("InvalidControlSum", "Sum of instructed amounts does not equal the control sum."),
+ AM11("InvalidTransactionCurrency", "Transaction currency is invalid or missing"),
+ AM12("InvalidAmount", "Amount is invalid or missing"),
+ AM13("AmountExceedsClearingSystemLimit", "Transaction amount exceeds limits set by clearing system"),
+ AM14("AmountExceedsAgreedLimit", "Transaction amount exceeds limits agreed between bank and client"),
+ AM15("AmountBelowClearingSystemMinimum", "Transaction amount below minimum set by clearing system"),
+ AM16("InvalidGroupControlSum", "Control Sum at the Group level is invalid"),
+ AM17("InvalidPaymentInfoControlSum", "Control Sum at the Payment Information level is invalid"),
+ AM18("InvalidNumberOfTransactions", "Number of transactions is invalid or missing."),
+ AM19("InvalidGroupNumberOfTransactions", "Number of transactions at the Group level is invalid or missing"),
+ AM20("InvalidPaymentInfoNumberOfTransactions", "Number of transactions at the Payment Information level is invalid"),
+ AM21("LimitExceeded", "Transaction amount exceeds limits agreed between bank and client."),
+ AM22("ZeroAmountNotApplied", "Unable to apply zero amount to designated account. For example, where the rules of a service allow the use of zero amount payments, however the back-office system is unable to apply the funds to the account. If the rules of a service prohibit the use of zero amount payments, then code AM01 is used to report the error condition."),
+ AM23("AmountExceedsSettlementLimit", "Transaction amount exceeds settlement limit."),
+ APAR("AlreadyPaidRTP", "Request To Pay has already been paid by the Debtor."),
+ ARFR("AlreadyRefusedRTP", "Request-to-pay has already been refused by the Debtor."),
+ ARJR("AlreadyRejectedRTP", "Request-to-pay has already been rejected."),
+ ATNS("AttachementsNotSupported", "Attachments to the request-to-pay are not supported."),
+ BE01("InconsistenWithEndCustomer", "Identification of end customer is not consistent with associated account number. (formerly CreditorConsistency)."),
+ BE04("MissingCreditorAddress", "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)."),
+ BE05("UnrecognisedInitiatingParty", "Party who initiated the message is not recognised by the end customer"),
+ BE06("UnknownEndCustomer", "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books"),
+ BE07("MissingDebtorAddress", "Specification of debtor's address, which is required for payment, is missing/not correct."),
+ BE08("MissingDebtorName", "Debtor name is missing"),
+ BE09("InvalidCountry", "Country code is missing or Invalid."),
+ BE10("InvalidDebtorCountry", "Debtor country code is missing or invalid"),
+ BE11("InvalidCreditorCountry", "Creditor country code is missing or invalid"),
+ BE12("InvalidCountryOfResidence", "Country code of residence is missing or Invalid."),
+ BE13("InvalidDebtorCountryOfResidence", "Country code of debtor's residence is missing or Invalid"),
+ BE14("InvalidCreditorCountryOfResidence", "Country code of creditor's residence is missing or Invalid"),
+ BE15("InvalidIdentificationCode", "Identification code missing or invalid."),
+ BE16("InvalidDebtorIdentificationCode", "Debtor or Ultimate Debtor identification code missing or invalid"),
+ BE17("InvalidCreditorIdentificationCode", "Creditor or Ultimate Creditor identification code missing or invalid"),
+ BE18("InvalidContactDetails", "Contact details missing or invalid"),
+ BE19("InvalidChargeBearerCode", "Charge bearer code for transaction type is invalid"),
+ BE20("InvalidNameLength", "Name length exceeds local rules for payment type."),
+ BE21("MissingName", "Name missing or invalid. Generic usage if cannot specifically identify debtor or creditor."),
+ BE22("MissingCreditorName", "Creditor name is missing"),
+ BE23("AccountProxyInvalid", "Phone number or email address, or any other proxy, used as the account proxy is unknown or invalid."),
+ CERI("CheckERI", "Credit transfer is not tagged as an Extended Remittance Information (ERI) transaction but contains ERI."),
+ CH03("RequestedExecutionDateOrRequestedCollectionDateTooFarInFuture", "Value in Requested Execution Date or Requested Collection Date is too far in the future"),
+ CH04("RequestedExecutionDateOrRequestedCollectionDateTooFarInPast", "Value in Requested Execution Date or Requested Collection Date is too far in the past"),
+ CH07("ElementIsNotToBeUsedAtB-andC-Level", "Element is not to be used at B- and C-Level"),
+ CH09("MandateChangesNotAllowed", "Mandate changes are not allowed"),
+ CH10("InformationOnMandateChangesMissing", "Information on mandate changes are missing"),
+ CH11("CreditorIdentifierIncorrect", "Value in Creditor Identifier is incorrect"),
+ CH12("CreditorIdentifierNotUnambiguouslyAtTransaction-Level", "Creditor Identifier is ambiguous at Transaction Level"),
+ CH13("OriginalDebtorAccountIsNotToBeUsed", "Original Debtor Account is not to be used"),
+ CH14("OriginalDebtorAgentIsNotToBeUsed", "Original Debtor Agent is not to be used"),
+ CH15("ElementContentIncludesMoreThan140Characters", "Content Remittance Information/Structured includes more than 140 characters"),
+ CH16("ElementContentFormallyIncorrect", "Content is incorrect"),
+ CH17("ElementNotAdmitted", "Element is not allowed"),
+ CH19("ValuesWillBeSetToNextTARGETday", "Values in Interbank Settlement Date or Requested Collection Date will be set to the next TARGET day"),
+ CH20("DecimalPointsNotCompatibleWithCurrency", "Number of decimal points not compatible with the currency"),
+ CH21("RequiredCompulsoryElementMissing", "Mandatory element is missing"),
+ CH22("COREandB2BwithinOnemessage", "SDD CORE and B2B not permitted within one message"),
+ CHQC("ChequeSettledOnCreditorAccount", "Cheque has been presented in cheque clearing and settled on the creditor’s account."),
+ CN01("AuthorisationCancelled", "Authorisation is cancelled."),
+ CNOR("CreditorBankIsNotRegistered", "Creditor bank is not registered under this BIC in the CSM"),
+ CURR("IncorrectCurrency", "Currency of the payment is incorrect"),
+ CUST("RequestedByCustomer", "Cancellation requested by the Debtor"),
+ DC02("SettlementNotReceived", "Rejection of a payment due to covering FI settlement not being received."),
+ DNOR("DebtorBankIsNotRegistered", "Debtor bank is not registered under this BIC in the CSM"),
+ DS01("ElectronicSignaturesCorrect", "The electronic signature(s) is/are correct"),
+ DS02("OrderCancelled", "An authorized user has cancelled the order"),
+ DS03("OrderNotCancelled", "The user’s attempt to cancel the order was not successful"),
+ DS04("OrderRejected", "The order was rejected by the bank side (for reasons concerning content)"),
+ DS05("OrderForwardedForPostprocessing", "The order was correct and could be forwarded for postprocessing"),
+ DS06("TransferOrder", "The order was transferred to VEU"),
+ DS07("ProcessingOK", "All actions concerning the order could be done by the EBICS bank server"),
+ DS08("DecompressionError", "The decompression of the file was not successful"),
+ DS09("DecryptionError", "The decryption of the file was not successful"),
+ DS0A("DataSignRequested", "Data signature is required."),
+ DS0B("UnknownDataSignFormat", "Data signature for the format is not available or invalid."),
+ DS0C("SignerCertificateRevoked", "The signer certificate is revoked."),
+ DS0D("SignerCertificateNotValid", "The signer certificate is not valid (revoked or not active)."),
+ DS0E("IncorrectSignerCertificate", "The signer certificate is not present."),
+ DS0F("SignerCertificationAuthoritySignerNotValid", "The authority of the signer certification sending the certificate is unknown."),
+ DS0G("NotAllowedPayment", "Signer is not allowed to sign this operation type."),
+ DS0H("NotAllowedAccount", "Signer is not allowed to sign for this account."),
+ DS0K("NotAllowedNumberOfTransaction", "The number of transaction is over the number allowed for this signer."),
+ DS10("Signer1CertificateRevoked", "The certificate is revoked for the first signer."),
+ DS11("Signer1CertificateNotValid", "The certificate is not valid (revoked or not active) for the first signer."),
+ DS12("IncorrectSigner1Certificate", "The certificate is not present for the first signer."),
+ DS13("SignerCertificationAuthoritySigner1NotValid", "The authority of signer certification sending the certificate is unknown for the first signer."),
+ DS14("UserDoesNotExist", "The user is unknown on the server"),
+ DS15("IdenticalSignatureFound", "The same signature has already been sent to the bank"),
+ DS16("PublicKeyVersionIncorrect", "The public key version is not correct. This code is returned when a customer sends signature files to the financial institution after conversion from an older program version (old ES format) to a new program version (new ES format) without having carried out re-initialisation with regard to a public key change."),
+ DS17("DifferentOrderDataInSignatures", "Order data and signatures don’t match"),
+ DS18("RepeatOrder", "File cannot be tested, the complete order has to be repeated. This code is returned in the event of a malfunction during the signature check, e.g. not enough storage space."),
+ DS19("ElectronicSignatureRightsInsufficient", "The user’s rights (concerning his signature) are insufficient to execute the order"),
+ DS20("Signer2CertificateRevoked", "The certificate is revoked for the second signer."),
+ DS21("Signer2CertificateNotValid", "The certificate is not valid (revoked or not active) for the second signer."),
+ DS22("IncorrectSigner2Certificate", "The certificate is not present for the second signer."),
+ DS23("SignerCertificationAuthoritySigner2NotValid", "The authority of signer certification sending the certificate is unknown for the second signer."),
+ DS24("WaitingTimeExpired", "Waiting time expired due to incomplete order"),
+ DS25("OrderFileDeleted", "The order file was deleted by the bank server"),
+ DS26("UserSignedMultipleTimes", "The same user has signed multiple times"),
+ DS27("UserNotYetActivated", "The user is not yet activated (technically)"),
+ DT01("InvalidDate", "Invalid date (eg, wrong or missing settlement date)"),
+ DT02("InvalidCreationDate", "Invalid creation date and time in Group Header (eg, historic date)"),
+ DT03("InvalidNonProcessingDate", "Invalid non bank processing date (eg, weekend or local public holiday)"),
+ DT04("FutureDateNotSupported", "Future date not supported"),
+ DT05("InvalidCutOffDate", "Associated message, payment information block or transaction was received after agreed processing cut-off date, i.e., date in the past."),
+ DT06("ExecutionDateChanged", "Execution Date has been modified in order for transaction to be processed"),
+ DU01("DuplicateMessageID", "Message Identification is not unique."),
+ DU02("DuplicatePaymentInformationID", "Payment Information Block is not unique."),
+ DU03("DuplicateTransaction", "Transaction is not unique."),
+ DU04("DuplicateEndToEndID", "End To End ID is not unique."),
+ DU05("DuplicateInstructionID", "Instruction ID is not unique."),
+ DUPL("DuplicatePayment", "Payment is a duplicate of another payment"),
+ ED01("CorrespondentBankNotPossible", "Correspondent bank not possible."),
+ ED03("BalanceInfoRequest", "Balance of payments complementary info is requested"),
+ ED05("SettlementFailed", "Settlement of the transaction has failed."),
+ ED06("SettlementSystemNotAvailable", "Interbank settlement system not available."),
+ EDTL("ExpiryDateTooLong", "Expiry date time of the request-to-pay is too far in the future."),
+ EDTR("ExpiryDateTimeReached", "Expiry date time of the request-to-pay is already reached."),
+ ERIN("ERIOptionNotSupported", "Extended Remittance Information (ERI) option is not supported."),
+ FF01("InvalidFileFormat", "File Format incomplete or invalid"),
+ FF02("SyntaxError", "Syntax error reason is provided as narrative information in the additional reason information."),
+ FF03("InvalidPaymentTypeInformation", "Payment Type Information is missing or invalid."),
+ FF04("InvalidServiceLevelCode", "Service Level code is missing or invalid"),
+ FF05("InvalidLocalInstrumentCode", "Local Instrument code is missing or invalid"),
+ FF06("InvalidCategoryPurposeCode", "Category Purpose code is missing or invalid"),
+ FF07("InvalidPurpose", "Purpose is missing or invalid"),
+ FF08("InvalidEndToEndId", "End to End Id missing or invalid"),
+ FF09("InvalidChequeNumber", "Cheque number missing or invalid"),
+ FF10("BankSystemProcessingError", "File or transaction cannot be processed due to technical issues at the bank side"),
+ FF11("ClearingRequestAborted", "Clearing request rejected due it being subject to an abort operation."),
+ FF12("OriginalTransactionNotEligibleForRequestedReturn", "Original payment is not eligible to be returned given its current status."),
+ FF13("RequestForCancellationNotFound", "No record of request for cancellation found."),
+ FOCR("FollowingCancellationRequest", "Return following a cancellation request."),
+ FR01("Fraud", "Returned as a result of fraud."),
+ FRAD("FraudulentOrigin", "Cancellation requested following a transaction that was originated fraudulently. The use of the FraudulentOrigin code should be governed by jurisdictions."),
+ G000("PaymentTransferredAndTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is tracked. No further updates will follow from the Status Originator."),
+ G001("PaymentTransferredAndNotTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is not tracked. No further updates will follow from the Status Originator."),
+ G002("CreditDebitNotConfirmed", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account may not be confirmed same day. Update will follow from the Status Originator."),
+ G003("CreditPendingDocuments", "In a FIToFI Customer Credit Transfer: Credit to creditor’s account is pending receipt of required documents. The Status Originator has requested creditor to provide additional documentation. Update will follow from the Status Originator."),
+ G004("CreditPendingFunds", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover. Update will follow from the Status Originator."),
+ G005("DeliveredWithServiceLevel", "Payment has been delivered to creditor agent with service level."),
+ G006("DeliveredWIthoutServiceLevel", "Payment has been delivered to creditor agent without service level."),
+ ID01("CorrespondingOriginalFileStillNotSent", "Signature file was sent to the bank but the corresponding original file has not been sent yet."),
+ IEDT("IncorrectExpiryDateTime", "Expiry date time of the request-to-pay is incorrect."),
+ IRNR("InitialRTPNeverReceived", "No initial request-to-pay has been received."),
+ MD01("NoMandate", "No Mandate"),
+ MD02("MissingMandatoryInformationInMandate", "Mandate related information data required by the scheme is missing."),
+ MD05("CollectionNotDue", "Creditor or creditor's agent should not have collected the direct debit"),
+ MD06("RefundRequestByEndCustomer", "Return of funds requested by end customer"),
+ MD07("EndCustomerDeceased", "End customer is deceased."),
+ MS02("NotSpecifiedReasonCustomerGenerated", "Reason has not been specified by end customer"),
+ MS03("NotSpecifiedReasonAgentGenerated", "Reason has not been specified by agent."),
+ NARR("Narrative", "Reason is provided as narrative information in the additional reason information."),
+ NERI("NoERI", "Credit transfer is tagged as an Extended Remittance Information (ERI) transaction but does not contain ERI."),
+ NOAR("NonAgreedRTP", "No existing agreement for receiving request-to-pay messages."),
+ NOAS("NoAnswerFromCustomer", "No response from Beneficiary."),
+ NOCM("NotCompliantGeneric", "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing."),
+ NOPG("NoPaymentGuarantee", "Requested payment guarantee (by Creditor) related to a request-to-pay cannot be provided."),
+ NRCH("PayerOrPayerRTPSPNotReachable", "Recipient side of the request-to-pay (payer or its request-to-pay service provider) is not reachable."),
+ PINS("TypeOfPaymentInstrumentNotSupported", "Type of payment requested in the request-to-pay is not supported by the payer."),
+ RC01("BankIdentifierIncorrect", "Bank identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)."),
+ RC02("InvalidBankIdentifier", "Bank identifier is invalid or missing."),
+ RC03("InvalidDebtorBankIdentifier", "Debtor bank identifier is invalid or missing"),
+ RC04("InvalidCreditorBankIdentifier", "Creditor bank identifier is invalid or missing"),
+ RC05("InvalidBICIdentifier", "BIC identifier is invalid or missing."),
+ RC06("InvalidDebtorBICIdentifier", "Debtor BIC identifier is invalid or missing"),
+ RC07("InvalidCreditorBICIdentifier", "Creditor BIC identifier is invalid or missing"),
+ RC08("InvalidClearingSystemMemberIdentifier", "ClearingSystemMemberidentifier is invalid or missing."),
+ RC09("InvalidDebtorClearingSystemMemberIdentifier", "Debtor ClearingSystemMember identifier is invalid or missing"),
+ RC10("InvalidCreditorClearingSystemMemberIdentifier", "Creditor ClearingSystemMember identifier is invalid or missing"),
+ RC11("InvalidIntermediaryAgent", "Intermediary Agent is invalid or missing"),
+ RC12("MissingCreditorSchemeId", "Creditor Scheme Id is invalid or missing"),
+ RCON("RMessageConflict", "Conflict with R-Message"),
+ RECI("ReceiverCustomerInformation", "Further information regarding the intended recipient."),
+ REPR("RTPReceivedCanBeProcessed", "Request-to-pay has been received and can be processed further."),
+ RF01("NotUniqueTransactionReference", "Transaction reference is not unique within the message."),
+ RR01("MissingDebtorAccountOrIdentification", "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing"),
+ RR02("MissingDebtorNameOrAddress", "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing."),
+ RR03("MissingCreditorNameOrAddress", "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing."),
+ RR04("RegulatoryReason", "Regulatory Reason"),
+ RR05("RegulatoryInformationInvalid", "Regulatory or Central Bank Reporting information missing, incomplete or invalid."),
+ RR06("TaxInformationInvalid", "Tax information missing, incomplete or invalid."),
+ RR07("RemittanceInformationInvalid", "Remittance information structure does not comply with rules for payment type."),
+ RR08("RemittanceInformationTruncated", "Remittance information truncated to comply with rules for payment type."),
+ RR09("InvalidStructuredCreditorReference", "Structured creditor reference invalid or missing."),
+ RR10("InvalidCharacterSet", "Character set supplied not valid for the country and payment type."),
+ RR11("InvalidDebtorAgentServiceID", "Invalid or missing identification of a bank proprietary service."),
+ RR12("InvalidPartyID", "Invalid or missing identification required within a particular country or payment type."),
+ RTNS("RTPNotSupportedForDebtor", "Debtor does not support request-to-pay transactions."),
+ RUTA("ReturnUponUnableToApply", "Return following investigation request and no remediation possible."),
+ S000("ValidRequestForCancellationAcknowledged", "Request for Cancellation is acknowledged following validation."),
+ S001("UETRFlaggedForCancellation", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been identified as being associated with a Request for Cancellation."),
+ S002("NetworkStopOfUETR", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been prevent from traveling across a messaging network."),
+ S003("RequestForCancellationForwarded", "Request for Cancellation has been forwarded to the payment processing/last payment processing agent."),
+ S004("RequestForCancellationDeliveryAcknowledgement", "Request for Cancellation has been acknowledged as delivered to payment processing/last payment processing agent."),
+ SL01("SpecificServiceOfferedByDebtorAgent", "Due to specific service offered by the Debtor Agent."),
+ SL02("SpecificServiceOfferedByCreditorAgent", "Due to specific service offered by the Creditor Agent."),
+ SL03("ServiceofClearingSystem", "Due to a specific service offered by the clearing system."),
+ SL11("CreditorNotOnWhitelistOfDebtor", "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet). In the Whitelist the Debtor may list all allowed Creditors to debit Debtor bank account."),
+ SL12("CreditorOnBlacklistOfDebtor", "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”. In the Blacklist the Debtor may list all Creditors not allowed to debit Debtor bank account."),
+ SL13("MaximumNumberOfDirectDebitTransactionsExceeded", "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent."),
+ SL14("MaximumDirectDebitTransactionAmountExceeded", "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent."),
+ SPII("RTPServiceProviderIdentifierIncorrect", "Identifier of the request-to-pay service provider is incorrect."),
+ TA01("TransmissonAborted", "The transmission of the file was not successful – it had to be aborted (for technical reasons)"),
+ TD01("NoDataAvailable", "There is no data available (for download)"),
+ TD02("FileNonReadable", "The file cannot be read (e.g. unknown format)"),
+ TD03("IncorrectFileStructure", "The file format is incomplete or invalid"),
+ TK01("TokenInvalid", "Token is invalid."),
+ TK02("SenderTokenNotFound", "Token used for the sender does not exist."),
+ TK03("ReceiverTokenNotFound", "Token used for the receiver does not exist."),
+ TK09("TokenMissing", "Token required for request is missing."),
+ TKCM("TokenCounterpartyMismatch", "Token found with counterparty mismatch."),
+ TKSG("TokenSingleUse", "Single Use Token already used."),
+ TKSP("TokenSuspended", "Token found with suspended status."),
+ TKVE("TokenValueLimitExceeded", "Token found with value limit rule violation."),
+ TKXP("TokenExpired", "Token expired."),
+ TM01("InvalidCutOffTime", "Associated message, payment information block, or transaction was received after agreed processing cut-off time."),
+ TS01("TransmissionSuccessful", "The (technical) transmission of the file was successful."),
+ TS04("TransferToSignByHand", "The order was transferred to pass by accompanying note signed by hand"),
+ UCRD("UnknownCreditor", "Unknown Creditor."),
+ UPAY("UnduePayment", "Payment is not justified."),
+}
+
+enum class ExternalPaymentGroupStatusCode(val isoCode: String, val description: String) {
+ ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."),
+ ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."),
+ ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement on the debtor's account has been completed."),
+ ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution."),
+ ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"),
+ ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."),
+ PART("PartiallyAccepted", "A number of transactions have been accepted, whereas another number of transactions have not yet achieved"),
+ PDNG("Pending", "Payment initiation or individual transaction included in the payment initiation is pending. Further checks and status update will be performed."),
+ RCVD("Received", "Payment initiation has been received by the receiving agent"),
+ RJCT("Rejected", "Payment initiation or individual transaction included in the payment initiation has been rejected."),
+}
+
+enum class ExternalPaymentTransactionStatusCode(val isoCode: String, val description: String) {
+ ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."),
+ ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."),
+ ACFC("AcceptedFundsChecked", "Preceding check of technical validation and customer profile was successful and an automatic funds check was positive."),
+ ACIS("AcceptedandChequeIssued", "Payment instruction to issue a cheque has been accepted, and the cheque has been issued but not yet been deposited or cleared."),
+ ACPD("AcceptedClearingProcessed", "Status of transaction released from the Debtor Agent and accepted by the clearing."),
+ ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement completed."),
+ ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment instruction has been accepted for execution."),
+ ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"),
+ ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."),
+ ACWP("AcceptedWithoutPosting", "Payment instruction included in the credit transfer is accepted without being posted to the creditor customer’s account."),
+ BLCK("Blocked", "Payment transaction previously reported with status 'ACWP' is blocked, for example, funds will neither be posted to the Creditor's account, nor be returned to the Debtor."),
+ CANC("Cancelled", "Payment initiation has been successfully cancelled after having received a request for cancellation."),
+ CPUC("CashPickedUpByCreditor", "Cash has been picked up by the Creditor."),
+ PATC("PartiallyAcceptedTechnicalCorrect", "Payment initiation needs multiple authentications, where some but not yet all have been performed. Syntactical and semantical validations are successful."),
+ PDNG("Pending", "Payment instruction is pending. Further checks and status update will be performed."),
+ PRES("Presented", "Request for Payment has been presented to the Debtor."),
+ RCVD("Received", "Payment instruction has been received."),
+ RJCT("Rejected", "Payment instruction has been rejected."),
+}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt
new file mode 100644
index 00000000..abc7bc80
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt
@@ -0,0 +1,35 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * 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/>
+ */
+
+package tech.libeufin.nexus
+
+enum class HacAction(val description: String) {
+ FILE_UPLOAD("File submitted to the bank"),
+ FILE_DOWNLOAD("File downloaded from the bank"),
+ ES_UPLOAD("Electronic signature submitted to the bank"),
+ ES_DOWNLOAD("Electronic signature downloaded from the bank"),
+ ES_VERIFICATION("Signature verification"),
+ VEU_FORWARDING("Forwarding to EDS"),
+ VEU_VERIFICATION("EDS signature verification"),
+ VEU_VERIFICATION_END("VEU_VERIFICATION_END"),
+ VEU_CANCEL_ORDER("Cancellation of EDS order"),
+ ADDITIONAL("Additional information"),
+ ORDER_HAC_FINAL_POS("HAC end of order (positive)"),
+ ORDER_HAC_FINAL_NEG("ORDER_HAC_FINAL_NEG")
+} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt
new file mode 100644
index 00000000..905bd223
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt
@@ -0,0 +1,209 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * 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/>
+ */
+
+package tech.libeufin.nexus
+
+import tech.libeufin.nexus.ebics.*
+import io.ktor.http.*
+import org.w3c.dom.Document
+import org.w3c.dom.Node
+import org.w3c.dom.NodeList
+import org.w3c.dom.ls.LSInput
+import org.w3c.dom.ls.LSResourceResolver
+import org.xml.sax.ErrorHandler
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.SAXParseException
+import java.io.*
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.security.interfaces.RSAPrivateCrtKey
+import javax.xml.XMLConstants
+import javax.xml.crypto.*
+import javax.xml.crypto.dom.DOMURIReference
+import javax.xml.crypto.dsig.*
+import javax.xml.crypto.dsig.dom.DOMSignContext
+import javax.xml.crypto.dsig.dom.DOMValidateContext
+import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec
+import javax.xml.crypto.dsig.spec.TransformParameterSpec
+import javax.xml.namespace.NamespaceContext
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.Source
+import javax.xml.transform.TransformerFactory
+import javax.xml.transform.dom.DOMSource
+import javax.xml.transform.stream.StreamResult
+import javax.xml.transform.stream.StreamSource
+import javax.xml.validation.SchemaFactory
+import javax.xml.validation.Validator
+import javax.xml.xpath.XPath
+import javax.xml.xpath.XPathConstants
+import javax.xml.xpath.XPathFactory
+
+/**
+ * This URI dereferencer allows handling the resource reference used for
+ * XML signatures in EBICS.
+ */
+private class EbicsSigUriDereferencer : URIDereferencer {
+ override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data {
+ if (myRef !is DOMURIReference)
+ throw Exception("invalid type")
+ if (myRef.uri != "#xpointer(//*[@authenticate='true'])")
+ throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'")
+ val xp: XPath = XPathFactory.newInstance().newXPath()
+ val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").evaluate(
+ myRef.here.ownerDocument, XPathConstants.NODESET
+ )
+ if (nodeSet !is NodeList)
+ throw Exception("invalid type")
+ if (nodeSet.length <= 0) {
+ throw Exception("no nodes to sign")
+ }
+ val nodeList = ArrayList<Node>()
+ for (i in 0 until nodeSet.length) {
+ val node = nodeSet.item(i)
+ nodeList.add(node)
+ }
+ return NodeSetData { nodeList.iterator() }
+ }
+}
+
+/**
+ * Helpers for dealing with XML in EBICS.
+ */
+object XMLUtil {
+ fun convertDomToBytes(document: Document): ByteArray {
+ val w = ByteArrayOutputStream()
+ val transformer = TransformerFactory.newInstance().newTransformer()
+ transformer.setOutputProperty(OutputKeys.STANDALONE, "yes")
+ transformer.transform(DOMSource(document), StreamResult(w))
+ return w.toByteArray()
+ }
+
+ /**
+ * Convert a node to a string without the XML declaration or
+ * indentation.
+ */
+ fun convertNodeToString(node: Node): String {
+ /* Make Transformer. */
+ val tf = TransformerFactory.newInstance()
+ val t = tf.newTransformer()
+ t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
+ /* Make string writer. */
+ val sw = StringWriter()
+ /* Extract string. */
+ t.transform(DOMSource(node), StreamResult(sw))
+ return sw.toString()
+ }
+
+ /** Parse [xml] into a XML DOM */
+ fun parseIntoDom(xml: InputStream): Document {
+ val factory = DocumentBuilderFactory.newInstance().apply {
+ isNamespaceAware = true
+ }
+ val builder = factory.newDocumentBuilder()
+ return xml.use {
+ builder.parse(InputSource(it))
+ }
+ }
+
+ /**
+ * Sign an EBICS document with the authentication and identity signature.
+ */
+ fun signEbicsDocument(
+ doc: Document,
+ signingPriv: PrivateKey,
+ withEbics3: Boolean = false
+ ) {
+ val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004"
+ val authSigNode = XPathFactory.newInstance().newXPath()
+ .evaluate("/*[1]/$ns:AuthSignature", doc, XPathConstants.NODE)
+ if (authSigNode !is Node)
+ throw java.lang.Exception("no AuthSignature")
+ val fac = XMLSignatureFactory.getInstance("DOM")
+ val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?)
+ val ref: Reference =
+ fac.newReference(
+ "#xpointer(//*[@authenticate='true'])",
+ fac.newDigestMethod(DigestMethod.SHA256, null),
+ listOf(c14n),
+ null,
+ null
+ )
+ val canon: CanonicalizationMethod =
+ fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?)
+ val signatureMethod = fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null)
+ val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref))
+ val sig: XMLSignature = fac.newXMLSignature(si, null)
+ val dsc = DOMSignContext(signingPriv, authSigNode)
+ dsc.defaultNamespacePrefix = "ds"
+ dsc.uriDereferencer = EbicsSigUriDereferencer()
+ dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true)
+ sig.sign(dsc)
+ val innerSig = authSigNode.firstChild
+ while (innerSig.hasChildNodes()) {
+ authSigNode.appendChild(innerSig.firstChild)
+ }
+ authSigNode.removeChild(innerSig)
+ }
+
+ fun verifyEbicsDocument(
+ doc: Document,
+ signingPub: PublicKey,
+ withEbics3: Boolean = false
+ ): Boolean {
+ val doc2: Document = doc.cloneNode(true) as Document
+ val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004"
+ val authSigNode = XPathFactory.newInstance().newXPath()
+ .evaluate("/*[1]/$ns:AuthSignature", doc2, XPathConstants.NODE)
+ if (authSigNode !is Node)
+ throw java.lang.Exception("no AuthSignature")
+ val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature")
+ authSigNode.parentNode.insertBefore(sigEl, authSigNode)
+ while (authSigNode.hasChildNodes()) {
+ sigEl.appendChild(authSigNode.firstChild)
+ }
+ authSigNode.parentNode.removeChild(authSigNode)
+ val fac = XMLSignatureFactory.getInstance("DOM")
+ val dvc = DOMValidateContext(signingPub, sigEl)
+ dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true)
+ dvc.uriDereferencer = EbicsSigUriDereferencer()
+ val sig = fac.unmarshalXMLSignature(dvc)
+ // FIXME: check that parameters are okay!
+ val valResult = sig.validate(dvc)
+ sig.signedInfo.references[0].validate(dvc)
+ return valResult
+ }
+
+ fun getNodeFromXpath(doc: Document, query: String): Node {
+ val xpath = XPathFactory.newInstance().newXPath()
+ val ret = xpath.evaluate(query, doc, XPathConstants.NODE)
+ ?: throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query")
+ return ret as Node
+ }
+
+ fun getStringFromXpath(doc: Document, query: String): String {
+ val xpath = XPathFactory.newInstance().newXPath()
+ val ret = xpath.evaluate(query, doc, XPathConstants.STRING) as String
+ if (ret.isEmpty()) {
+ throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query")
+ }
+ return ret
+ }
+} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt
new file mode 100644
index 00000000..a0d806ec
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt
@@ -0,0 +1,204 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2020, 2024 Taler Systems S.A.
+ *
+ * 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/>
+ */
+
+package tech.libeufin.nexus
+
+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
+
+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)
+ }
+ lambda()
+ path.splitToSequence('/').forEach {
+ w.writeEndElement()
+ }
+ }
+
+ override fun attr(namespace: String, name: String, value: String) {
+ w.writeAttribute(namespace, name, value)
+ }
+
+ override fun attr(name: String, value: String) {
+ w.writeAttribute(name, value)
+ }
+
+ override fun text(content: String) {
+ w.writeCharacters(content)
+ }
+}
+
+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)
+
+private fun Element.childrenByTag(tag: String): Sequence<Element> = sequence {
+ for (i in 0..childNodes.length) {
+ val el = childNodes.item(i)
+ if (el !is Element) {
+ continue
+ }
+ if (el.localName != tag) {
+ continue
+ }
+ yield(el)
+ }
+}
+
+class XmlDestructor internal constructor(private val el: Element) {
+ fun each(path: String, f: XmlDestructor.() -> Unit) {
+ el.childrenByTag(path).forEach {
+ f(XmlDestructor(it))
+ }
+ }
+
+ fun <T> map(path: String, f: XmlDestructor.() -> T): List<T> {
+ return el.childrenByTag(path).map {
+ f(XmlDestructor(it))
+ }.toList()
+ }
+
+ fun one(path: String): XmlDestructor {
+ val children = el.childrenByTag(path).iterator()
+ if (!children.hasNext()) {
+ throw DestructionError("expected a single $path child, got none instead at $el")
+ }
+ val el = children.next()
+ if (children.hasNext()) {
+ throw DestructionError("expected a single $path child, got ${children.asSequence() + 1} instead at $el")
+ }
+ return XmlDestructor(el)
+ }
+ fun opt(path: String): XmlDestructor? {
+ val children = el.childrenByTag(path).iterator()
+ if (!children.hasNext()) {
+ return null
+ }
+ val el = children.next()
+ if (children.hasNext()) {
+ throw DestructionError("expected an optional $path child, got ${children.asSequence().count() + 1} instead at $el")
+ }
+ return XmlDestructor(el)
+ }
+
+ fun <T> one(path: String, f: XmlDestructor.() -> T): T = f(one(path))
+ fun <T> opt(path: String, f: XmlDestructor.() -> T): T? = opt(path)?.run(f)
+
+ fun text(): String = el.textContent
+ fun bool(): Boolean = el.textContent.toBoolean()
+ fun date(): LocalDate = LocalDate.parse(text(), DateTimeFormatter.ISO_DATE)
+ fun dateTime(): LocalDateTime = LocalDateTime.parse(text(), DateTimeFormatter.ISO_DATE_TIME)
+ inline fun <reified T : Enum<T>> enum(): T = java.lang.Enum.valueOf(T::class.java, text())
+
+ fun attr(index: String): String = el.getAttribute(index)
+
+ companion object {
+ fun <T> fromStream(xml: InputStream, root: String, f: XmlDestructor.() -> T): T {
+ val doc = XMLUtil.parseIntoDom(xml)
+ return fromDoc(doc, root, f)
+ }
+
+ fun <T> fromDoc(doc: Document, root: String, f: XmlDestructor.() -> T): T {
+ if (doc.documentElement.tagName != root) {
+ throw DestructionError("expected root '$root' got '${doc.documentElement.tagName}'")
+ }
+ val destr = XmlDestructor(doc.documentElement)
+ return f(destr)
+ }
+ }
+}
+
+fun <T> destructXml(xml: InputStream, root: String, f: XmlDestructor.() -> T): T
+ = XmlDestructor.fromStream(xml, root, f)
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..6cd031ac
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt
@@ -0,0 +1,288 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * 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 is the main "EBICS library interface". Functions here are stateless helpers
+ * used to implement both an EBICS server and EBICS client.
+ */
+
+package tech.libeufin.nexus.ebics
+
+import io.ktor.http.*
+import org.w3c.dom.Document
+import tech.libeufin.common.crypto.CryptoUtil
+import tech.libeufin.common.*
+import tech.libeufin.nexus.*
+import java.io.InputStream
+import java.security.SecureRandom
+import java.security.interfaces.RSAPrivateCrtKey
+import java.security.interfaces.RSAPublicKey
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import javax.xml.datatype.DatatypeFactory
+import javax.xml.datatype.XMLGregorianCalendar
+
+data class EbicsProtocolError(
+ val httpStatusCode: HttpStatusCode,
+ val reason: String,
+ /**
+ * This class is also used when Nexus finds itself
+ * in an inconsistent state, without interacting with the
+ * bank. In this case, the EBICS code below can be left
+ * null.
+ */
+ val ebicsTechnicalCode: EbicsReturnCode? = null
+) : Exception(reason)
+
+/**
+ * @param size in bits
+ */
+fun getNonce(size: Int): ByteArray {
+ val sr = SecureRandom()
+ val ret = ByteArray(size / 8)
+ sr.nextBytes(ret)
+ return ret
+}
+
+data class PreparedUploadData(
+ val transactionKey: ByteArray,
+ val userSignatureDataEncrypted: ByteArray,
+ val dataDigest: ByteArray,
+ val encryptedPayloadChunks: List<String>
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PreparedUploadData
+
+ if (!transactionKey.contentEquals(other.transactionKey)) return false
+ if (!userSignatureDataEncrypted.contentEquals(other.userSignatureDataEncrypted)) return false
+ if (encryptedPayloadChunks != other.encryptedPayloadChunks) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = transactionKey.contentHashCode()
+ result = 31 * result + userSignatureDataEncrypted.contentHashCode()
+ result = 31 * result + encryptedPayloadChunks.hashCode()
+ return result
+ }
+}
+
+data class DataEncryptionInfo(
+ val transactionKey: ByteArray,
+ val bankPubDigest: ByteArray
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as DataEncryptionInfo
+
+ if (!transactionKey.contentEquals(other.transactionKey)) return false
+ if (!bankPubDigest.contentEquals(other.bankPubDigest)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = transactionKey.contentHashCode()
+ result = 31 * result + bankPubDigest.contentHashCode()
+ return result
+ }
+}
+
+
+// TODO import missing using a script
+@Suppress("SpellCheckingInspection")
+enum class EbicsReturnCode(val errorCode: String) {
+ EBICS_OK("000000"),
+ EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"),
+ EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"),
+ EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"),
+ EBICS_AUTHENTICATION_FAILED("061001"),
+ EBICS_INVALID_REQUEST("061002"),
+ EBICS_INTERNAL_ERROR("061099"),
+ EBICS_TX_RECOVERY_SYNC("061101"),
+ EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"),
+ EBICS_INVALID_ORDER_DATA_FORMAT("090004"),
+ EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"),
+ EBICS_INVALID_USER_OR_USER_STATE("091002"),
+ EBICS_USER_UNKNOWN("091003"),
+ EBICS_EBICS_INVALID_USER_STATE("091004"),
+ EBICS_INVALID_ORDER_IDENTIFIER("091005"),
+ EBICS_UNSUPPORTED_ORDER_TYPE("091006"),
+ EBICS_INVALID_XML("091010"),
+ EBICS_TX_MESSAGE_REPLAY("091103"),
+ EBICS_PROCESSING_ERROR("091116"),
+ EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"),
+ EBICS_AMOUNT_CHECK_FAILED("091303");
+
+ companion object {
+ fun lookup(errorCode: String): EbicsReturnCode {
+ for (x in entries) {
+ if (x.errorCode == errorCode) {
+ return x
+ }
+ }
+ throw Exception(
+ "Unknown EBICS status code: $errorCode"
+ )
+ }
+ }
+}
+
+
+fun signOrderEbics3(
+ orderBlob: ByteArray,
+ signKey: RSAPrivateCrtKey,
+ partnerId: String,
+ userId: String
+): ByteArray {
+ return XmlBuilder.toString("UserSignatureData") {
+ attr("xmlns", "http://www.ebics.org/S002")
+ el("OrderSignatureData") {
+ el("SignatureVersion", "A006")
+ el("SignatureValue", CryptoUtil.signEbicsA006(
+ CryptoUtil.digestEbicsOrderA006(orderBlob),
+ signKey
+ ).encodeBase64())
+ el("PartnerID", partnerId)
+ el("UserID", userId)
+ }
+ }.toByteArray()
+}
+
+data class EbicsResponseContent(
+ val transactionID: String?,
+ val orderID: String?,
+ val dataEncryptionInfo: DataEncryptionInfo?,
+ val orderDataEncChunk: String?,
+ val technicalReturnCode: EbicsReturnCode,
+ val bankReturnCode: EbicsReturnCode,
+ val reportText: String,
+ val segmentNumber: Int?,
+ // Only present in init phase
+ val numSegments: Int?
+)
+
+data class EbicsKeyManagementResponseContent(
+ val technicalReturnCode: EbicsReturnCode,
+ val bankReturnCode: EbicsReturnCode?,
+ val orderData: ByteArray?
+)
+
+
+class HpbResponseData(
+ val hostID: String,
+ val encryptionPubKey: RSAPublicKey,
+ val encryptionVersion: String,
+ val authenticationPubKey: RSAPublicKey,
+ val authenticationVersion: String
+)
+
+
+fun ebics3toInternalRepr(response: Document): EbicsResponseContent {
+ // TODO better ebics response type
+ return XmlDestructor.fromDoc(response, "ebicsResponse") {
+ var transactionID: String? = null
+ var numSegments: Int? = null
+ lateinit var technicalReturnCode: EbicsReturnCode
+ lateinit var bankReturnCode: EbicsReturnCode
+ lateinit var reportText: String
+ var orderID: String? = null
+ var segmentNumber: Int? = null
+ var orderDataEncChunk: String? = null
+ var dataEncryptionInfo: DataEncryptionInfo? = null
+ one("header") {
+ one("static") {
+ transactionID = opt("TransactionID")?.text()
+ numSegments = opt("NumSegments")?.text()?.toInt()
+ }
+ one("mutable") {
+ segmentNumber = opt("SegmentNumber")?.text()?.toInt()
+ orderID = opt("OrderID")?.text()
+ technicalReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ reportText = one("ReportText").text()
+ }
+ }
+ one("body") {
+ opt("DataTransfer") {
+ orderDataEncChunk = one("OrderData").text()
+ dataEncryptionInfo = opt("DataEncryptionInfo") {
+ DataEncryptionInfo(
+ one("TransactionKey").text().decodeBase64(),
+ one("EncryptionPubKeyDigest").text().decodeBase64()
+ )
+ }
+ }
+ bankReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ }
+ EbicsResponseContent(
+ transactionID = transactionID,
+ orderID = orderID,
+ bankReturnCode = bankReturnCode,
+ technicalReturnCode = technicalReturnCode,
+ reportText = reportText,
+ orderDataEncChunk = orderDataEncChunk,
+ dataEncryptionInfo = dataEncryptionInfo,
+ numSegments = numSegments,
+ segmentNumber = segmentNumber
+ )
+ }
+}
+
+fun parseEbicsHpbOrder(orderDataRaw: InputStream): HpbResponseData {
+ return XmlDestructor.fromStream(orderDataRaw, "HPBResponseOrderData") {
+ val (authenticationPubKey, authenticationVersion) = one("AuthenticationPubKeyInfo") {
+ Pair(
+ one("PubKeyValue").one("RSAKeyValue") {
+ CryptoUtil.loadRsaPublicKeyFromComponents(
+ one("Modulus").text().decodeBase64(),
+ one("Exponent").text().decodeBase64(),
+ )
+ },
+ one("AuthenticationVersion").text()
+ )
+ }
+ val (encryptionPubKey, encryptionVersion) = one("EncryptionPubKeyInfo") {
+ Pair(
+ one("PubKeyValue").one("RSAKeyValue") {
+ CryptoUtil.loadRsaPublicKeyFromComponents(
+ one("Modulus").text().decodeBase64(),
+ one("Exponent").text().decodeBase64(),
+ )
+ },
+ one("EncryptionVersion").text()
+ )
+
+ }
+ val hostID: String = one("HostID").text()
+ HpbResponseData(
+ hostID = hostID,
+ encryptionPubKey = encryptionPubKey,
+ encryptionVersion = encryptionVersion,
+ authenticationPubKey = authenticationPubKey,
+ authenticationVersion = authenticationVersion
+ )
+ }
+} \ 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
index 1fdb4b26..0879d3c3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
@@ -26,7 +26,7 @@ package tech.libeufin.nexus.ebics
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.common.*
-import tech.libeufin.ebics.*
+import tech.libeufin.nexus.*
import tech.libeufin.nexus.BankPublicKeysFile
import tech.libeufin.nexus.ClientPrivateKeysFile
import tech.libeufin.nexus.EbicsSetupConfig
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 e7df9c83..993c209c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
@@ -19,13 +19,9 @@
package tech.libeufin.nexus.ebics
import io.ktor.client.*
-import tech.libeufin.ebics.*
+import tech.libeufin.nexus.*
import tech.libeufin.common.*
import tech.libeufin.common.crypto.*
-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.*
import java.time.format.*
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
index 496006e2..228580d3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -45,7 +45,6 @@ import io.ktor.http.*
import io.ktor.utils.io.jvm.javaio.*
import tech.libeufin.common.*
import tech.libeufin.common.crypto.*
-import tech.libeufin.ebics.*
import tech.libeufin.nexus.*
import java.io.ByteArrayOutputStream
import java.io.InputStream
diff --git a/nexus/src/test/kotlin/Ebics.kt b/nexus/src/test/kotlin/Ebics.kt
index 7bd05fa7..e2a2ab84 100644
--- a/nexus/src/test/kotlin/Ebics.kt
+++ b/nexus/src/test/kotlin/Ebics.kt
@@ -20,7 +20,7 @@
import io.ktor.client.engine.mock.*
import io.ktor.http.*
import org.junit.Test
-import tech.libeufin.ebics.XMLUtil
+import tech.libeufin.nexus.*
import tech.libeufin.nexus.ebics.*
import kotlin.io.path.Path
import kotlin.io.path.writeBytes
diff --git a/nexus/src/test/kotlin/XmlCombinatorsTest.kt b/nexus/src/test/kotlin/XmlCombinatorsTest.kt
new file mode 100644
index 00000000..b14920e3
--- /dev/null
+++ b/nexus/src/test/kotlin/XmlCombinatorsTest.kt
@@ -0,0 +1,76 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * 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/>
+ */
+
+import org.junit.Test
+import tech.libeufin.nexus.XmlBuilder
+import tech.libeufin.nexus.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) TODO fix empty tag being closed only with toString
+ assertEquals(expected, XMLUtil.convertDomToBytes(toDom).toString(Charsets.UTF_8))
+ }
+
+ @Test
+ fun testWithModularity() {
+ fun module(base: XmlBuilder) {
+ base.el("module")
+ }
+ testBuilder(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><root><module/></root>",
+ "root"
+ ) {
+ module(this)
+ }
+ }
+
+ @Test
+ fun testWithIterable() {
+ 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("e$i/e$i$i", "$i$i$i")
+ }
+ }
+ }
+
+ @Test
+ fun testBasicXmlBuilding() {
+ 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") {
+ attr("nested", "true")
+ el("g/h")
+ }
+ }
+ el("one_more")
+ }
+ }
+}
diff --git a/nexus/src/test/kotlin/XmlUtilTest.kt b/nexus/src/test/kotlin/XmlUtilTest.kt
new file mode 100644
index 00000000..f847c928
--- /dev/null
+++ b/nexus/src/test/kotlin/XmlUtilTest.kt
@@ -0,0 +1,73 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * 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/>
+ */
+
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import tech.libeufin.common.crypto.CryptoUtil
+import tech.libeufin.common.decodeBase64
+import tech.libeufin.nexus.XMLUtil
+import java.security.KeyPairGenerator
+import javax.xml.transform.stream.StreamSource
+
+class XmlUtilTest {
+
+ @Test
+ fun basicSigningTest() {
+ val doc = XMLUtil.parseIntoDom("""
+ <myMessage xmlns:ebics="urn:org:ebics:H004">
+ <ebics:AuthSignature />
+ <foo authenticate="true">Hello World</foo>
+ </myMessage>
+ """.trimIndent().toByteArray().inputStream())
+ val kpg = KeyPairGenerator.getInstance("RSA")
+ kpg.initialize(2048)
+ val pair = kpg.genKeyPair()
+ val otherPair = kpg.genKeyPair()
+ XMLUtil.signEbicsDocument(doc, pair.private)
+ kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public))
+ kotlin.test.assertFalse(XMLUtil.verifyEbicsDocument(doc, otherPair.public))
+ }
+
+ @Test
+ fun multiAuthSigningTest() {
+ val doc = XMLUtil.parseIntoDom("""
+ <myMessage xmlns:ebics="urn:org:ebics:H004">
+ <ebics:AuthSignature />
+ <foo authenticate="true">Hello World</foo>
+ <bar authenticate="true">Another one!</bar>
+ </myMessage>
+ """.trimIndent().toByteArray().inputStream())
+ val kpg = KeyPairGenerator.getInstance("RSA")
+ kpg.initialize(2048)
+ val pair = kpg.genKeyPair()
+ XMLUtil.signEbicsDocument(doc, pair.private)
+ kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public))
+ }
+
+ @Test
+ fun testRefSignature() {
+ val classLoader = ClassLoader.getSystemClassLoader()
+ val docText = classLoader.getResourceAsStream("signature1/doc.xml")
+ val doc = XMLUtil.parseIntoDom(docText)
+ val keyStream = classLoader.getResourceAsStream("signature1/public_key.txt")
+ val keyBytes = keyStream.decodeBase64().readAllBytes()
+ val key = CryptoUtil.loadRsaPublicKey(keyBytes)
+ assertTrue(XMLUtil.verifyEbicsDocument(doc, key))
+ }
+} \ No newline at end of file
diff --git a/nexus/src/test/resources/signature1/doc.xml b/nexus/src/test/resources/signature1/doc.xml
new file mode 100644
index 00000000..271f8429
--- /dev/null
+++ b/nexus/src/test/resources/signature1/doc.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<myMessage xmlns:ebics="urn:org:ebics:H004">
+ <ebics:AuthSignature><ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#xpointer(//*[@authenticate='true'])"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>qiFUoCn9kE0zSidyraO2Br/wn3/XyvWObJZ0aLIBXyA=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#">LupLyRUJIuk0kCRwpFj4fpen2MI7Jw0BI944agwzXHfSDfq0Pp8h3sub6eSsKIAq7ekT3z+mlfMc&#13;
+ VFaKRi4B7kv4ja/URiYCKKbChQU2+kMGDvsncx9VcpcFrqAbWPmE9JXD2W2YW9OSkJ1tAZxZlZwS&#13;
+ A8KcvluV1wGEBuakHL2t3GqFPQEfKW4l8GYTjHh/w9jBve5d8tvMOjGtoyNemZGrVlzBxO9+hwbw&#13;
+ 8UFUCDA00dCjFDUHOnyAbBYsGzoaQyZprDn3iYDvlBz243zAN98PIKDclxlUEmkuF+JhrhCRjT9l&#13;
+ +JJxrELGHaDkFVadR4kaPdWPsbDaV0/2Fzc4Qg==</ds:SignatureValue></ebics:AuthSignature>
+ <foo authenticate="true">Hello World</foo>
+</myMessage> \ No newline at end of file
diff --git a/nexus/src/test/resources/signature1/public_key.txt b/nexus/src/test/resources/signature1/public_key.txt
new file mode 100644
index 00000000..6d52df58
--- /dev/null
+++ b/nexus/src/test/resources/signature1/public_key.txt
@@ -0,0 +1 @@
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqpUpetHZYdMjnaG544iSLZ5SnxlV4F/eQsIckG3mvMaXCQsY4rUTfJyle/fTZ0xGbjCUXCsbl1wkz8eB6chaX2LsHYDGiu/xNnU1nddAVB+5kkA5AIGncT9NVhdOgmpnZY/tae9qtZfCPAvbI0sGYQHea0pwyJ/hUnRJiMOjSRgIXALIvGVNqxe4U5ffLXFIUapTK2hOuhUH9BwDSK+mVR6gw0vDT05Z38sEpTeKUqJywL5cPSFIV+AN4ErSvsXNkTKUcbDxhGzOh/oTjTkz1kFFKe4ijPkSRkpK2sJMyAIretBKOK8SDICnsSrIh0YAcd6yTHQ3CeEjW4t0ZBULOQIDAQAB \ No newline at end of file