libeufin

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

commit c4db134d9a8934e5348aaae2f543556010aeb16a
parent e3613d161e17748a97e99f27b6b2c17242602ecc
Author: Antoine A <>
Date:   Tue, 22 Oct 2024 17:32:04 +0200

common: check edsa key validity in subject parser

Diffstat:
Mbank/src/test/kotlin/CoreBankApiTest.kt | 2+-
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 8++++----
Mcommon/src/main/kotlin/TalerCommon.kt | 8++++++++
Mcommon/src/main/kotlin/TxMedatada.kt | 15+++++++++++----
Mcommon/src/test/kotlin/TxMedataTest.kt | 5+++--
Mnexus/src/test/kotlin/DatabaseTest.kt | 4++--
Mnexus/src/test/kotlin/helpers.kt | 6+++---
Mtestbench/src/test/kotlin/IntegrationTest.kt | 6+++---
8 files changed, 35 insertions(+), 19 deletions(-)

diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -1260,7 +1260,7 @@ class CoreBankTransactionsApiTest { assertBalance("exchange", "+KUDOS:0") tx("merchant", "KUDOS:1", "exchange", "") // Bounce common to transaction tx("merchant", "KUDOS:1", "exchange", "Malformed") // Bounce malformed transaction - val reservePub = EddsaPublicKey.rand() + val reservePub = EddsaPublicKey.randEdsaKey() tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Accept incoming tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Bounce reserve_pub reuse assertBalance("merchant", "-KUDOS:1") diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -230,7 +230,7 @@ class WireGatewayApiTest { { addIncoming("KUDOS:10") }, // Reserve transactions using raw bank transaction logic - { tx("merchant", "KUDOS:10", "exchange", "history test with ${ShortHashCode.rand()} reserve pub") }, + { tx("merchant", "KUDOS:10", "exchange", "history test with ${EddsaPublicKey.randEdsaKey()} reserve pub") }, // Reserve transactions using withdraw logic { withdrawal("KUDOS:9") }, @@ -239,7 +239,7 @@ class WireGatewayApiTest { { addKyc("KUDOS:2") }, // KYC transactions using raw bank transaction logic - { tx("merchant", "KUDOS:2", "exchange", "history test with KYC:${ShortHashCode.rand()} account pub") }, + { tx("merchant", "KUDOS:2", "exchange", "history test with KYC:${EddsaPublicKey.randEdsaKey()} account pub") }, ), ignored = listOf( // Ignore malformed incoming transaction @@ -368,8 +368,8 @@ class WireGatewayApiTest { fun addIncomingMix() = bankSetup { addIncoming("KUDOS:1") addKyc("KUDOS:2") - tx("merchant", "KUDOS:3", "exchange", "test with ${ShortHashCode.rand()} reserve pub") - tx("merchant", "KUDOS:4", "exchange", "test with KYC:${ShortHashCode.rand()} account pub") + tx("merchant", "KUDOS:3", "exchange", "test with ${EddsaPublicKey.randEdsaKey()} reserve pub") + tx("merchant", "KUDOS:4", "exchange", "test with KYC:${EddsaPublicKey.randEdsaKey()} account pub") client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=25").assertOkJson<IncomingHistory> { assertEquals(4, it.incoming_transactions.size) it.incoming_transactions.forEachIndexed { i, tx -> diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt @@ -36,6 +36,7 @@ import java.net.URI import java.net.URL import java.time.Instant import java.time.temporal.ChronoUnit +import org.bouncycastle.math.ec.rfc8032.Ed25519 sealed class CommonError(msg: String): Exception(msg) { class AmountFormat(msg: String): CommonError(msg) @@ -518,6 +519,13 @@ class Base32Crockford32B { companion object { fun rand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).rand()) fun secureRand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).secureRand()) + fun randEdsaKey(): EddsaPublicKey { + val secretKey = ByteArray(32) + Ed25519.generatePrivateKey(SECURE_RNG.get(), secretKey) + val publicKey = ByteArray(32) + Ed25519.generatePublicKey(secretKey, 0, publicKey, 0) + return Base32Crockford32B(publicKey) + } } } diff --git a/common/src/main/kotlin/TxMedatada.kt b/common/src/main/kotlin/TxMedatada.kt @@ -19,6 +19,8 @@ package tech.libeufin.common +import org.bouncycastle.math.ec.rfc8032.Ed25519 + private val PART_PATTERN = Regex("[:a-z0-9A-Z]*") private val BASE32_32B_PATTERN = Regex("(KYC:)?([a-z0-9A-Z]{52})") @@ -40,17 +42,22 @@ fun parseIncomingTxMetadata(subject: String): TalerIncomingMetadata { return null } - val match = iterator.next() + val (prefix, base32) = iterator.next().destructured // Check many if (iterator.hasNext()) { throw Exception("Found multiple reserve public key") } - // Check kind - val (prefix, key) = match.destructured + // Check key validity + val key = EddsaPublicKey(base32) + if (!Ed25519.validatePublicKeyFull(key.raw, 0)) { + return null + } + + // Check key type val type = if (prefix == "KYC:") TalerIncomingType.kyc else TalerIncomingType.reserve - return TalerIncomingMetadata(type, EddsaPublicKey(key)) + return TalerIncomingMetadata(type, key) } // Wire transfer subjects are generally small in size, and not diff --git a/common/src/test/kotlin/TxMedataTest.kt b/common/src/test/kotlin/TxMedataTest.kt @@ -86,8 +86,9 @@ class TxMetadataTest { // Check failure if malformed or missing for (case in sequenceOf( - "does not contain any reserve", // Check fail if none - upper.substring(0, upper.length-1) // Check fail if missing char + "does not contain any reserve", // Check fail if none + upper.substring(0, upper.length-1), // Check fail if missing char + "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" // Check fail if not a valid key )) { assertFailsMsg("Missing reserve public key") { parseIncomingTxMetadata(case) diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt @@ -208,7 +208,7 @@ class IncomingPaymentsTest { @Test fun reconcileMissingId() = setup { db, _ -> val cfg = NexusIngestConfig.default(AccountType.exchange) - val subject = "test with ${ShortHashCode.rand()} reserve pub" + val subject = "test with ${EddsaPublicKey.randEdsaKey()} reserve pub" // Register with missing ID val incoming = genInPay(subject) @@ -252,7 +252,7 @@ class IncomingPaymentsTest { @Test fun reconcileMissingIdKyc() = setup { db, _ -> val cfg = NexusIngestConfig.default(AccountType.exchange) - val subject = "test with KYC:${ShortHashCode.rand()} account pub" + val subject = "test with KYC:${EddsaPublicKey.randEdsaKey()} account pub" // Register with missing ID val incoming = genInPay(subject) diff --git a/nexus/src/test/kotlin/helpers.kt b/nexus/src/test/kotlin/helpers.kt @@ -161,13 +161,13 @@ suspend fun ApplicationTestBuilder.addKyc(amount: String) { /** Register a talerable outgoing transaction */ suspend fun talerableOut(db: Database) { - val wtid = ShortHashCode.rand() + val wtid = EddsaPublicKey.randEdsaKey() registerOutgoingPayment(db, genOutPay("$wtid http://exchange.example.com/")) } /** Register a talerable reserve incoming transaction */ suspend fun talerableIn(db: Database, nullId: Boolean = false, amount: String = "CHF:44") { - val reserve_pub = ShortHashCode.rand() + val reserve_pub = EddsaPublicKey.randEdsaKey() registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), genInPay("test with $reserve_pub reserve pub", amount).run { if (nullId) { @@ -181,7 +181,7 @@ suspend fun talerableIn(db: Database, nullId: Boolean = false, amount: String = /** Register a talerable KYC incoming transaction */ suspend fun talerableKycIn(db: Database, nullId: Boolean = false, amount: String = "CHF:44") { - val account_pub = ShortHashCode.rand() + val account_pub = EddsaPublicKey.randEdsaKey() registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), genInPay("test with KYC:$account_pub account pub", amount).run { if (nullId) { diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt @@ -160,7 +160,7 @@ class IntegrationTest { it.execSQLUpdate("SET search_path TO libeufin_nexus;") } - val reservePub = EddsaPublicKey.rand() + val reservePub = EddsaPublicKey.randEdsaKey() val reservePayment = IncomingPayment( amount = TalerAmount("EUR:10"), debtorPayto = userPayTo, @@ -178,7 +178,7 @@ class IntegrationTest { db, cfg, reservePayment.copy( bankId = "kyc", - subject = "Error test KYC:${EddsaPublicKey.rand()}" + subject = "Error test KYC:${EddsaPublicKey.randEdsaKey()}" ) ) @@ -302,7 +302,7 @@ class IntegrationTest { // Cashin repeat(3) { i -> - val reservePub = EddsaPublicKey.rand() + val reservePub = EddsaPublicKey.randEdsaKey() val amount = TalerAmount("EUR:${20+i}") val subject = "cashin test $i: $reservePub" nexusCmd.run("testing fake-incoming $flags --subject \"$subject\" --amount $amount $userPayTo")