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:
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")