gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-kotlin] branch master updated (86bd043 -> ba7e1cc)


From: gnunet
Subject: [taler-wallet-kotlin] branch master updated (86bd043 -> ba7e1cc)
Date: Mon, 29 Jun 2020 21:00:46 +0200

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

torsten-grote pushed a change to branch master
in repository wallet-kotlin.

    from 86bd043  Add Planchet creation with tests and platform specific secure 
random bytes
     new cd46828  Add verification methods for various signatures
     new ba7e1cc  Add signing of RecoupRequest with tests

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .idea/dictionaries/user.xml                        |   1 +
 .../net/taler/wallet/kotlin/Base32Crockford.kt     |   2 +-
 .../kotlin/net/taler/wallet/kotlin/Types.kt        | 192 +++++++++++++
 .../net/taler/wallet/kotlin/crypto/Planchet.kt     |   5 +-
 .../net/taler/wallet/kotlin/crypto/Recoup.kt       |  67 +++++
 .../net/taler/wallet/kotlin/crypto/Signature.kt    |  95 ++++++-
 .../net/taler/wallet/kotlin/crypto/RecoupTest.kt   |  74 +++++
 .../taler/wallet/kotlin/crypto/SignatureTest.kt    | 309 +++++++++++++++++++++
 8 files changed, 742 insertions(+), 3 deletions(-)
 create mode 100644 src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
 create mode 100644 
src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt
 create mode 100644 
src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt

diff --git a/.idea/dictionaries/user.xml b/.idea/dictionaries/user.xml
index c5ce0d6..69ce746 100644
--- a/.idea/dictionaries/user.xml
+++ b/.idea/dictionaries/user.xml
@@ -5,6 +5,7 @@
       <w>eddsa</w>
       <w>hmac</w>
       <w>nacl</w>
+      <w>payto</w>
       <w>planchet</w>
       <w>planchets</w>
       <w>taler</w>
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt
index c966af2..3bcf15a 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt
@@ -106,7 +106,7 @@ object Base32Crockford {
      * @param stringSize size of the string to decode
      * @return size of the resulting data in bytes
      */
-    private fun calculateDecodedDataLength(stringSize: Int): Int {
+    fun calculateDecodedDataLength(stringSize: Int): Int {
         return stringSize * 5 / 8
     }
 
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
new file mode 100644
index 0000000..2aa44da
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
@@ -0,0 +1,192 @@
+package net.taler.wallet.kotlin
+
+data class WireFee(
+    /**
+     * Fee for wire transfers.
+     */
+    val wireFee: Amount,
+    /**
+     * Fees to close and refund a reserve.
+     */
+    val closingFee: Amount,
+    /**
+     * Start date of the fee.
+     */
+    val startStamp: Timestamp,
+    /**
+     * End date of the fee.
+     */
+    val endStamp: Timestamp,
+    /**
+     * Signature made by the exchange master key.
+     */
+    val signature: String
+)
+
+data class DenominationRecord(
+    /**
+     * Value of one coin of the denomination.
+     */
+    val value: Amount,
+    /**
+     * The denomination public key.
+     */
+    val denomPub: String,
+    /**
+     * Hash of the denomination public key.
+     * Stored in the database for faster lookups.
+     */
+    val denomPubHash: String,
+    /**
+     * Fee for withdrawing.
+     */
+    val feeWithdraw: Amount,
+    /**
+     * Fee for depositing.
+     */
+    val feeDeposit: Amount,
+    /**
+     * Fee for refreshing.
+     */
+    val feeRefresh: Amount,
+    /**
+     * Fee for refunding.
+     */
+    val feeRefund: Amount,
+    /**
+     * Validity start date of the denomination.
+     */
+    val stampStart: Timestamp,
+    /**
+     * Date after which the currency can't be withdrawn anymore.
+     */
+    val stampExpireWithdraw: Timestamp,
+    /**
+     * Date after the denomination officially doesn't exist anymore.
+     */
+    val stampExpireLegal: Timestamp,
+    /**
+     * Data after which coins of this denomination can't be deposited anymore.
+     */
+    val stampExpireDeposit: Timestamp,
+    /**
+     * Signature by the exchange's master key over the denomination
+     * information.
+     */
+    val masterSig: String,
+    /**
+     * Did we verify the signature on the denomination?
+     */
+    val status: DenominationStatus,
+    /**
+     * Was this denomination still offered by the exchange the last time
+     * we checked?
+     * Only false when the exchange redacts a previously published 
denomination.
+     */
+    val isOffered: Boolean,
+    /**
+     * Did the exchange revoke the denomination?
+     * When this field is set to true in the database, the same transaction
+     * should also mark all affected coins as revoked.
+     */
+    val isRevoked: Boolean,
+    /**
+     * Base URL of the exchange.
+     */
+    val exchangeBaseUrl: String
+)
+
+enum class DenominationStatus {
+    /**
+     * Verification was delayed.
+     */
+    Unverified,
+
+    /**
+     * Verified as valid.
+     */
+    VerifiedGood,
+
+    /**
+     * Verified as invalid.
+     */
+    VerifiedBad
+}
+
+class CoinRecord(
+    /**
+     * Where did the coin come from?  Used for recouping coins.
+     */
+    val coinSource: CoinSourceType,
+
+    /**
+     * Public key of the coin.
+     */
+    val coinPub: String,
+
+    /**
+     * Private key to authorize operations on the coin.
+     */
+    val coinPriv: String,
+
+    /**
+     * Key used by the exchange used to sign the coin.
+     */
+    val denomPub: String,
+
+    /**
+     * Hash of the public key that signs the coin.
+     */
+    val denomPubHash: String,
+
+    /**
+     * Unblinded signature by the exchange.
+     */
+    val denomSig: String,
+
+    /**
+     * Amount that's left on the coin.
+     */
+    val currentAmount: Amount,
+
+    /**
+     * Base URL that identifies the exchange from which we got the coin.
+     */
+    val exchangeBaseUrl: String,
+
+    /**
+     * The coin is currently suspended, and will not be used for payments.
+     */
+    val suspended: Boolean,
+
+    /**
+     * Blinding key used when withdrawing the coin.
+     * Potentially send again during payback.
+     */
+    val blindingKey: String,
+
+    /**
+     * Status of the coin.
+     */
+    val status: CoinStatus
+)
+
+enum class CoinSourceType(val value: String) {
+    WITHDRAW("withdraw"),
+    REFRESH("refresh"),
+    TIP("tip")
+}
+
+enum class CoinStatus(val value: String) {
+
+    /**
+     * Withdrawn and never shown to anybody.
+     */
+    FRESH("fresh"),
+
+    /**
+     * A coin that has been spent and refreshed.
+     */
+    DORMANT("dormant")
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
index 617441d..8f4fb98 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
@@ -26,7 +26,7 @@ internal class Planchet(private val crypto: Crypto) {
         val coinEvHash: String
     )
 
-    fun create(req: CreationRequest, coinKeyPair: EddsaKeyPair, 
blindingFactor: ByteArray): CreationResult {
+    internal fun create(req: CreationRequest, coinKeyPair: EddsaKeyPair, 
blindingFactor: ByteArray): CreationResult {
         val reservePub = Base32Crockford.decode(req.reservePub)
         val reservePriv = Base32Crockford.decode(req.reservePriv)
         val denomPub = Base32Crockford.decode(req.denomPub)
@@ -59,6 +59,9 @@ internal class Planchet(private val crypto: Crypto) {
         )
     }
 
+    /**
+     * Create a pre-coin ([Planchet]) of the given [CreationRequest].
+     */
     fun create(req: CreationRequest): CreationResult {
         val coinKeyPair = crypto.createEddsaKeyPair()
         val blindingFactor = crypto.getRandomBytes(32)
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt
new file mode 100644
index 0000000..79612a8
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt
@@ -0,0 +1,67 @@
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.CoinSourceType.REFRESH
+import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_RECOUP
+
+internal class Recoup(private val crypto: Crypto) {
+
+    /**
+     * Request that we send to the exchange to get a payback.
+     */
+    data class Request(
+        /**
+         * Hashed denomination public key of the coin we want to get
+         * paid back.
+         */
+        val denomPubHash: String,
+
+        /**
+         * Signature over the coin public key by the denomination.
+         */
+        val denomSig: String,
+
+        /**
+         * Coin public key of the coin we want to refund.
+         */
+        val coinPub: String,
+
+        /**
+         * Blinding key that was used during withdraw,
+         * used to prove that we were actually withdrawing the coin.
+         */
+        val coinBlindKeySecret: String,
+
+        /**
+         * Signature made by the coin, authorizing the payback.
+         */
+        val coinSig: String,
+
+        /**
+         * Was the coin refreshed (and thus the recoup should go to the old 
coin)?
+         */
+        val refreshed: Boolean
+    )
+
+    /**
+     * Create and sign a message to recoup a coin.
+     */
+    fun createRequest(coin: CoinRecord): Request {
+        val p = Signature.PurposeBuilder(WALLET_COIN_RECOUP)
+            .put(Base32Crockford.decode(coin.coinPub))
+            .put(Base32Crockford.decode(coin.denomPubHash))
+            .put(Base32Crockford.decode(coin.blindingKey))
+            .build()
+        val coinSig = crypto.eddsaSign(p, 
Base32Crockford.decode(coin.coinPriv))
+        return Request(
+            coinBlindKeySecret = coin.blindingKey,
+            coinPub = coin.coinPub,
+            coinSig = Base32Crockford.encode(coinSig),
+            denomPubHash = coin.denomPubHash,
+            denomSig = coin.denomSig,
+            refreshed = coin.coinSource === REFRESH
+        )
+    }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
index 30db04f..c86942e 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
@@ -1,9 +1,13 @@
 package net.taler.wallet.kotlin.crypto
 
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.DenominationRecord
+import net.taler.wallet.kotlin.WireFee
 import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
 
-class Signature {
+internal class Signature(private val crypto: Crypto) {
 
+    @Suppress("unused")
     companion object {
         const val RESERVE_WITHDRAW = 1200
         const val WALLET_COIN_DEPOSIT = 1201
@@ -43,4 +47,93 @@ class Signature {
         }
     }
 
+    private fun verifyPayment(sig: ByteArray, contractHash: ByteArray, 
merchantPub: ByteArray): Boolean {
+        val p = PurposeBuilder(MERCHANT_PAYMENT_OK)
+            .put(contractHash)
+            .build()
+        return crypto.eddsaVerify(p, sig, merchantPub)
+    }
+
+    /**
+     * Verifies an EdDSA payment signature made with [MERCHANT_PAYMENT_OK].
+     *
+     * @param merchantPub an EdDSA public key, usually belonging to a merchant.
+     *
+     * @return true if the signature is valid, false otherwise
+     */
+    fun verifyPayment(sig: String, contractHash: String, merchantPub: String): 
Boolean {
+        val sigBytes = Base32Crockford.decode(sig)
+        val hashBytes = Base32Crockford.decode(contractHash)
+        val pubBytes = Base32Crockford.decode(merchantPub)
+        return verifyPayment(sigBytes, hashBytes, pubBytes)
+    }
+
+    /**
+     * Verifies an EdDSA wire fee signature made with [MASTER_WIRE_FEES].
+     *
+     * @param masterPub an EdDSA public key
+     *
+     * @return true if the signature is valid, false otherwise
+     */
+    fun verifyWireFee(type: String, wireFee: WireFee, masterPub: String): 
Boolean {
+        val p = PurposeBuilder(MASTER_WIRE_FEES)
+            .put(crypto.sha512("$type\u0000".encodeToByteArray()))
+            .put(wireFee.startStamp.roundedToByteArray())
+            .put(wireFee.endStamp.roundedToByteArray())
+            .put(wireFee.wireFee.toByteArray())
+            .put(wireFee.closingFee.toByteArray())
+            .build()
+        val sig = Base32Crockford.decode(wireFee.signature)
+        val pub = Base32Crockford.decode(masterPub)
+        return crypto.eddsaVerify(p, sig, pub)
+    }
+
+    /**
+     * Verifies an EdDSA denomination record signature made with 
[MASTER_DENOMINATION_KEY_VALIDITY].
+     *
+     * @param masterPub an EdDSA public key
+     *
+     * @return true if the signature is valid, false otherwise
+     */
+    fun verifyDenominationRecord(d: DenominationRecord, masterPub: String): 
Boolean {
+        val pub = Base32Crockford.decode(masterPub)
+        val p = PurposeBuilder(MASTER_DENOMINATION_KEY_VALIDITY)
+            .put(pub)
+            .put(d.stampStart.roundedToByteArray())
+            .put(d.stampExpireWithdraw.roundedToByteArray())
+            .put(d.stampExpireDeposit.roundedToByteArray())
+            .put(d.stampExpireLegal.roundedToByteArray())
+            .put(d.value.toByteArray())
+            .put(d.feeWithdraw.toByteArray())
+            .put(d.feeDeposit.toByteArray())
+            .put(d.feeRefresh.toByteArray())
+            .put(d.feeRefund.toByteArray())
+            .put(Base32Crockford.decode(d.denomPubHash))
+            .build()
+        val sig = Base32Crockford.decode(d.masterSig)
+        return crypto.eddsaVerify(p, sig, pub)
+    }
+
+    /**
+     * Verifies an EdDSA wire account signature made with 
[MASTER_WIRE_DETAILS].
+     *
+     * @param masterPub an EdDSA public key
+     *
+     * @return true if the signature is valid, false otherwise
+     */
+    fun verifyWireAccount(paytoUri: String, signature: String, masterPub: 
String): Boolean {
+        val h = crypto.kdf(
+            64,
+            "exchange-wire-signature".encodeToByteArray(),
+            "$paytoUri\u0000".encodeToByteArray(),
+            ByteArray(0)
+        )
+        val p = PurposeBuilder(MASTER_WIRE_DETAILS)
+            .put(h)
+            .build()
+        val sig = Base32Crockford.decode(signature)
+        val pub = Base32Crockford.decode(masterPub)
+        return crypto.eddsaVerify(p, sig, pub)
+    }
+
 }
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt
new file mode 100644
index 0000000..865eaa9
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt
@@ -0,0 +1,74 @@
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.CoinSourceType.REFRESH
+import net.taler.wallet.kotlin.CoinSourceType.WITHDRAW
+import net.taler.wallet.kotlin.CoinStatus.FRESH
+import net.taler.wallet.kotlin.crypto.Recoup.Request
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class RecoupTest {
+
+    private val crypto = CryptoFactory.getCrypto()
+    private val recoup = Recoup(crypto)
+
+    private class RecoupRequestVector(val record: CoinRecord, val request: 
Request)
+
+    @Test
+    fun test() {
+        val vectors = listOf(
+            RecoupRequestVector(
+                CoinRecord(
+                    coinSource = WITHDRAW,
+                    coinPub = 
"9YW99NYH54FWG87TP3SKCGR9MRWYSVR75X42FN4YAJC9579CQBJ0",
+                    coinPriv = 
"EPPYWTDVWM4CXW75J8AAGWW620C7DCC3B45TM31KHKPYMT9VM7DG",
+                    denomPub = 
"020000X3T40FNGSM3Y1QFKX9H4JY5EP70Y2CKDHD29B5BEZCTWRMT6AC9SA0G5YJ1XVYY580K6S93SFCKM5PFKP96H3KXDNP58EVQPTYDB5S0QY4V8B873NYA7EYRH25NJ8MR2VP6F7WWVMBK3NR3FSFP17PHPGF279NBSRTXWZSJZFX6RCTR6VS5WMSYFHZCR0P8R6MGHDCB3QW4M3G2001",
+                    denomPubHash = 
"DG3114X57XKHQ1XM6AN0P7D2B6J96SVFG09S3SF43ZXCYYK9PGX84XP3ZY7WY3QD9JE1BWS2T8DGR78QXZZAVGED79HES10HAPTWBX8",
+                    denomSig = 
"AHE8DGMTTKNWGCVQYTV56CBWA81DH10BQEBAM0A5YGRAZXRPVHMZ5FH0XW1523QXSTXT3WMS1X7FDMEZ3BR898YEDTXDTHEMX6RS11KCPBAZCGTNPHKYF6RH9414Q0PYT5BZKGKWJNAFPWQS715NXEFZBY1D6RPTAN520REJ4RTREC9PP5D8WVQ3B66Q4ARYQ3CK49K0ZDME0",
+                    currentAmount = Amount("TESTKUDOS", 0, 0),
+                    exchangeBaseUrl = "example.org",
+                    suspended = false,
+                    blindingKey = 
"1Y29A3ABERGYJR8Y9HS7XS8AYYDAKV6BZSXMZ0WS5VDTS150C100",
+                    status = FRESH
+                ),
+                Request(
+                    denomPubHash = 
"DG3114X57XKHQ1XM6AN0P7D2B6J96SVFG09S3SF43ZXCYYK9PGX84XP3ZY7WY3QD9JE1BWS2T8DGR78QXZZAVGED79HES10HAPTWBX8",
+                    denomSig = 
"AHE8DGMTTKNWGCVQYTV56CBWA81DH10BQEBAM0A5YGRAZXRPVHMZ5FH0XW1523QXSTXT3WMS1X7FDMEZ3BR898YEDTXDTHEMX6RS11KCPBAZCGTNPHKYF6RH9414Q0PYT5BZKGKWJNAFPWQS715NXEFZBY1D6RPTAN520REJ4RTREC9PP5D8WVQ3B66Q4ARYQ3CK49K0ZDME0",
+                    coinPub = 
"9YW99NYH54FWG87TP3SKCGR9MRWYSVR75X42FN4YAJC9579CQBJ0",
+                    coinBlindKeySecret = 
"1Y29A3ABERGYJR8Y9HS7XS8AYYDAKV6BZSXMZ0WS5VDTS150C100",
+                    coinSig = 
"GBN5MVEY6JATGGSTX5YF32G3G204Y1PF9ASVXQFN895DWN5ZK3CBY2NHC8ATB1E9JWSV1QD4ECM0XHP8Y6DFZ1S02MYD5NBKZ45B018",
+                    refreshed = false
+                )
+            ),
+            RecoupRequestVector(
+                CoinRecord(
+                    coinSource = REFRESH,
+                    coinPub = 
"2YE003173JB6WNQ9HS73Z468F11KDHWWVGCPDHDTD6AY5AVJPQPG",
+                    coinPriv = 
"GCR4R26XTCFNS109ZYC0G6M374K1ACNES1YH2CESWD86JBAE22WG",
+                    denomPub = 
"020000X9M8MQVNH28D4J4YFA5ZZNKGNR0423BXQZV00RRN754XTDQMS5YKWQ3KSN8NV4V7CHDM22CRJ4WWQW05FDZC7VN0KK4S8VK9PYPPXNW6FJKHBSEZ2X1FCJKRC3T6PK2BKQ422Y2ASE76ZZAH6RRQT4SQGZTV3TRTSBC5AECJ5Z6C4RX7XFBERKVB45DA7H3V53YCYX1C41ZY5G2001",
+                    denomPubHash = 
"J0G3G880JJJD09923AAWNQQZHJVRQT71ZK8KZGYW7T1P18PCPZ72FBAKDW3EFZ3QFZEW72EYJ9K0FG3RFZTFADQKZDDN9YT6BT2PE70",
+                    denomSig = 
"8HVKAGMKRQRWB1HX9WCPX3FJ0SVE24DCAWQSHX4ZMXZ1KFZDNF4F0Z4K6ZCW142B2WDEH0W848W8WKH8P6A6EJR7J635QEF78CSJFF0EX1FRS5VY484GEX0HH3BDRDFGTHXNQRTTF1DD5ETMEG1QNKA3SAB24XZXZNQ6RDGTK02MRETP859NGMDD2F94F58JH4HYGXMAY0X32",
+                    currentAmount = Amount("TESTKUDOS", 0, 0),
+                    exchangeBaseUrl = "example.org",
+                    suspended = false,
+                    blindingKey = 
"C5VPT5F925ADJWK48PR07KV2W66EZQN4KYE146NY77DFM8GFCTXG",
+                    status = FRESH
+                ),
+                Request(
+                    denomPubHash = 
"J0G3G880JJJD09923AAWNQQZHJVRQT71ZK8KZGYW7T1P18PCPZ72FBAKDW3EFZ3QFZEW72EYJ9K0FG3RFZTFADQKZDDN9YT6BT2PE70",
+                    denomSig = 
"8HVKAGMKRQRWB1HX9WCPX3FJ0SVE24DCAWQSHX4ZMXZ1KFZDNF4F0Z4K6ZCW142B2WDEH0W848W8WKH8P6A6EJR7J635QEF78CSJFF0EX1FRS5VY484GEX0HH3BDRDFGTHXNQRTTF1DD5ETMEG1QNKA3SAB24XZXZNQ6RDGTK02MRETP859NGMDD2F94F58JH4HYGXMAY0X32",
+                    coinPub = 
"2YE003173JB6WNQ9HS73Z468F11KDHWWVGCPDHDTD6AY5AVJPQPG",
+                    coinBlindKeySecret = 
"C5VPT5F925ADJWK48PR07KV2W66EZQN4KYE146NY77DFM8GFCTXG",
+                    coinSig = 
"HGPAWTM2ZXVBZWYVSPS6S9DMSQWSVEJCQ78BN6WG2VND3PA7BQNHVE6142CGYX0VA82G5YP9SAV5YDNPNQJH2FTY5M6VM92QF6CB228",
+                    refreshed = true
+                )
+            )
+        )
+        for (v in vectors) {
+            assertEquals(v.request, recoup.createRequest(v.record))
+        }
+    }
+
+}
diff --git 
a/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
index 1326cc4..48cbc8d 100644
--- a/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
@@ -16,13 +16,25 @@
 
 package net.taler.wallet.kotlin.crypto
 
+import net.taler.wallet.kotlin.Amount
 import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.DenominationRecord
+import net.taler.wallet.kotlin.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.DenominationStatus.VerifiedBad
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.WireFee
 import net.taler.wallet.kotlin.crypto.Signature.PurposeBuilder
+import kotlin.random.Random
 import kotlin.test.Test
 import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
 
 class SignatureTest {
 
+    private val crypto = CryptoFactory.getCrypto()
+    private val signature = Signature(crypto)
+
     private class PurposeBuilderVector(val purposeNum: Int, val chunks: 
List<String>, val result: String)
 
     @Test
@@ -181,4 +193,301 @@ class SignatureTest {
         }
     }
 
+    private class PaymentSignatureVector(val publicKey: String, val hash: 
String, val signature: String)
+
+    @Test
+    fun testVerifyPaymentSignature() {
+        val vectors = listOf(
+            PaymentSignatureVector(
+                "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0",
+                
"CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR",
+                
"JSNG99MX5W4AS7AEA8D4ADCHYTHFER0GX1N064E1XX48N513AXAEHDTG8ZT7ANWQK5HGCAXGEWN7TCBTYVG3RDPBTAS5HEP608KQ40R"
+            ),
+            PaymentSignatureVector(
+                "6WC3MPYM5XKPKRA2Z6SYB81CPFV3E7EC6S2GVE095X8XH63QTZCG",
+                
"Z6H76JXPJFP3JBGSF54XBF0BVXDJ0CJBK4YT9GVR1AT916ZD57KP53YZN5G67A4YN95WGMZKQW7744483P5JDF06B6S7TMK195QGP20",
+                
"T2Y4KJJPZ0F2DMNF5S81V042T20VHB5VRXQYX4RF8KRH6H2Z4JRBD05CCDJ6C625MHM5FQET00RDX2NF5QX63S9YDXEP0710VBYHY10"
+            ),
+            PaymentSignatureVector(
+                "2X3PSPT7D6TEM97R98C0DHZREFVAVA3XTH11D5A2Z2K7GBKQ7AEG",
+                
"JSNG99MX5W4AS7AEA8D4ADCHYTHFER0GX1N064E1XX48N513AXAEHDTG8ZT7ANWQK5HGCAXGEWN7TCBTYVG3RDPBTAS5HEP608KQ40R",
+                
"W0783H1KZ6GX58T5ZEH5VYMTXP7P7EA3KBKQ4Y8CN8M20GY8RNA4RX1AZG6TQ70NR4XG4EZ9D606P4RDAARD2SXTKA90BJMN9VEAC00"
+            )
+        )
+        for (v in vectors) {
+            // verification succeeds as expected
+            assertTrue(signature.verifyPayment(v.signature, v.hash, 
v.publicKey))
+            // verification fails which different signature
+            val size = 
Base32Crockford.calculateDecodedDataLength(v.signature.length)
+            val sig = Base32Crockford.encode(Random.nextBytes(size))
+            assertFalse(signature.verifyPayment(sig, v.hash, v.publicKey))
+            // verification fails which different hash
+            val hash = Base32Crockford.encode(Random.nextBytes(64))
+            assertFalse(signature.verifyPayment(v.signature, hash, 
v.publicKey))
+            // verification fails which different public key
+            val publicKey = Base32Crockford.encode(Random.nextBytes(32))
+            assertFalse(signature.verifyPayment(v.signature, v.hash, 
publicKey))
+        }
+    }
+
+    private class WireFeeSignatureVector(val wireFee: WireFee, val masterPub: 
String)
+
+    @Test
+    fun testVerifyWireFeeSignature() {
+        val type = "x-taler-bank"
+        val vectors = listOf(
+            WireFeeSignatureVector(
+                WireFee(
+                    wireFee = Amount("TESTKUDOS", 0, 1000000),
+                    closingFee = Amount("TESTKUDOS", 0, 1000000),
+                    startStamp = Timestamp(1609470000000),
+                    endStamp = Timestamp(1641006000000),
+                    signature = 
"C77EZ56ZT4ACNPGP3PX881S42413N37NJVTRPNMM9BWRVGQBK2C157HHRF61RMYPAWW6QVWV5RVKEVBD3XAVJFXAEBM2HPNS5AMPY18"
+                ), "Y8JGNCPMM7XTF44FT7V1JRNNJQ6F4R7DZPXAKYYCJZJVEX2C45QG"
+            ),
+            WireFeeSignatureVector(
+                WireFee(
+                    wireFee = Amount("TESTKUDOS", 0, 1000000),
+                    closingFee = Amount("TESTKUDOS", 0, 1000000),
+                    startStamp = Timestamp(1577847600000),
+                    endStamp = Timestamp(1609470000000),
+                    signature = 
"4F87Z12WQM7JXEKPRPGFZVGCZPNN6Q9RVP2NA0PZ57CYQP4QXV38EQW59X3K5WAQN82BX95X57775ZJGA5EB3VA5GV9S3JBA3RGX41G"
+                ), "0BJ4SX13W4Q64QDDSRBVTYSWT8C0X2HVB61QYSZM1B1W9JVCE0C0"
+            ),
+            WireFeeSignatureVector(
+                WireFee(
+                    wireFee = Amount("TESTKUDOS", 0, 1000000),
+                    closingFee = Amount("TESTKUDOS", 0, 1000000),
+                    startStamp = Timestamp(1672542000000),
+                    endStamp = Timestamp(1704078000000),
+                    signature = 
"9YWES9YPR2W5ACCYE4ZE4S3PE62CVX01AXXSC9KT9PZ6B52D5GSBP4DG95Y024EDE5HFGP6FEZVPKQM8PA6J9WD2NXW3QBA34MEV61R"
+                ), "FWPD4E312RB8XZ22FGHCZGV2YPT7AX02XVRZEC7F53QKZJHY7H50"
+            )
+        )
+        for (v in vectors) {
+            // verification succeeds as expected
+            assertTrue(signature.verifyWireFee(type, v.wireFee, v.masterPub))
+            // different type fails verification
+            assertFalse(signature.verifyWireFee("foo", v.wireFee, v.masterPub))
+            // different WireFee wireFee fails verification
+            var wireFee = v.wireFee.copy(wireFee = Amount("TESTKUDOS", 0, 100))
+            assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+            // different WireFee closingFee fails verification
+            wireFee = v.wireFee.copy(closingFee = Amount("TESTKUDOS", 0, 100))
+            assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+            // different WireFee startStamp fails verification
+            wireFee = v.wireFee.copy(startStamp = 
Timestamp(v.wireFee.startStamp.ms + 1000))
+            assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+            // different WireFee endStamp fails verification
+            wireFee = v.wireFee.copy(endStamp = 
Timestamp(v.wireFee.endStamp.ms + 1000))
+            assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+            // different WireFee signature fails verification
+            val size = 
Base32Crockford.calculateDecodedDataLength(v.wireFee.signature.length)
+            wireFee = v.wireFee.copy(signature = 
Base32Crockford.encode(Random.nextBytes(size)))
+            assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+            // startStamp changes below one second don't affect verification
+            wireFee = v.wireFee.copy(startStamp = 
Timestamp(v.wireFee.startStamp.ms + 999))
+            assertTrue(signature.verifyWireFee(type, wireFee, v.masterPub))
+            // startStamp changes below one second don't affect verification
+            wireFee = v.wireFee.copy(endStamp = 
Timestamp(v.wireFee.endStamp.ms + 999))
+            assertTrue(signature.verifyWireFee(type, wireFee, v.masterPub))
+            // different masterPub fails verification
+            val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+            assertFalse(signature.verifyWireFee(type, v.wireFee, masterPub))
+        }
+    }
+
+    private class DenominationRecordSignatureVector(val denominationRecord: 
DenominationRecord, val masterPub: String)
+
+    @Test
+    fun testVerifyDenominationRecordSignature() {
+        val vectors = listOf(
+            DenominationRecordSignatureVector(
+                DenominationRecord(
+                    value = Amount("TESTKUDOS", 4, 0),
+                    denomPub = 
"020000XX26WN5X7ECKERVGBWTFM3KJ7AT0N8T7RQB7W7G4Q5K0W1BT8QFQBMMTR925TC6RX4QGVVVXH2ZMJVWDRR58BRXNDCFBZTH7RNS1KVWZQGEWME1D6QX79R0V6V9S0NC9H8YP0W6MJD7YSV5VQZWCR1JXRTSS0HPHDV4V6AVX34TSMHGRDQXZYVTEBGVWNF8TNR2P5296TCZR0G2001",
+                    denomPubHash = 
"MY54RXQ1WTZPFD3VZ4QJH6RHFPTYE78Y4DQ3GANTWK2Z0SQ99AK90K5H0P419EY4QWV6S4Q6QG52HYFCVRCPEQNG22RM3E7XW2YFJ9R",
+                    feeWithdraw = Amount("TESTKUDOS", 0, 3000000),
+                    feeDeposit = Amount("TESTKUDOS", 0, 3000000),
+                    feeRefresh = Amount("TESTKUDOS", 0, 4000000),
+                    feeRefund = Amount("TESTKUDOS", 0, 2000000),
+                    stampStart = Timestamp(1593449948000),
+                    stampExpireWithdraw = Timestamp(1594054748000),
+                    stampExpireLegal = Timestamp(1688057948000),
+                    stampExpireDeposit = Timestamp(1656521948000),
+                    masterSig = 
"2PEF4EHTKE2R89KHQBZ6NAQW34YEMAP044DTJXJ9TFX224YW07BB2X1VHEYH425N2RNDZS9XFEAQK54GV6Q6CTNSE1Z038TZ4V3MM1R",
+                    status = Unverified, isOffered = false, isRevoked = false, 
exchangeBaseUrl = "example.org"
+                ),
+                "C5K3YXPHSDVYPCB79D5AC4FYCKQ9D0DN3N3MBA5D54BG42SX0ZRG"
+            ),
+            DenominationRecordSignatureVector(
+                DenominationRecord(
+                    value = Amount("TESTKUDOS", 0, 10000000),
+                    denomPub = 
"020000XWWS2DFXM1G8YR6NXVF07R0DH7GRE1J7ZC2YENN7Q60ZHX9S7FEVQDBFP1041DN0GFASZR6A7RSJ3EYRV1YD5ACAZRDQNH5P5KEBTSK5XA76YYWTW5VA7N1V1VRVF0CF7VZ8GSJNQ91FSJGNKGR82DB7H82TPAGMR1B5DG5SY2WP3NB5YJD90H64E09C0YC8FCCWRGC09HFNPG2001",
+                    denomPubHash = 
"J73HZJTR76GRZKBEKQC4X289056V5RMY4JS932XM8BAENQ83YAF9W2WYKRBN87TN8ENXP61JC7HMC7PYK9MZ8S289FCD07EM95AJGMR",
+                    feeWithdraw = Amount("TESTKUDOS", 0, 1000000),
+                    feeDeposit = Amount("TESTKUDOS", 0, 1000000),
+                    feeRefresh = Amount("TESTKUDOS", 0, 3000000),
+                    feeRefund = Amount("TESTKUDOS", 0, 1000000),
+                    stampStart = Timestamp(1593450307000),
+                    stampExpireWithdraw = Timestamp(1594055107000),
+                    stampExpireLegal = Timestamp(1688058307000),
+                    stampExpireDeposit = Timestamp(1656522307000),
+                    masterSig = 
"HPBAY19C1B5H3FAYWKRQFS8VC693658SNSK3BB304BNAG950BS881GMVPVN0YBG67K9J9E4A9BFN7VAQEY1D5G6YAGVPFQWX0GRQ62G",
+                    status = Unverified, isOffered = false, isRevoked = false, 
exchangeBaseUrl = "example.org"
+                ),
+                "NF8G14QFVWJSQPNFC3M2JKNMGJESS8ZWZX9V43PG40YXWRWA88VG"
+            )
+        )
+        for (v in vectors) {
+            // verification succeeds as expected
+            
assertTrue(signature.verifyDenominationRecord(v.denominationRecord, 
v.masterPub))
+            // different masterPub fails verification
+            val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+            
assertFalse(signature.verifyDenominationRecord(v.denominationRecord, masterPub))
+            // different value fails verification
+            val value = v.denominationRecord.value + 
Amount.min(v.denominationRecord.value.currency)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(value = value),
+                    v.masterPub
+                )
+            )
+            // different denomPubHash fails verification
+            val calculatedDenomPubHash = 
crypto.sha512(Base32Crockford.decode(v.denominationRecord.denomPub))
+            assertEquals(v.denominationRecord.denomPubHash, 
Base32Crockford.encode(calculatedDenomPubHash))
+            val denomPubHash = 
Base32Crockford.encode(Random.nextBytes(calculatedDenomPubHash.size))
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(denomPubHash = denomPubHash),
+                    v.masterPub
+                )
+            )
+            // different feeWithdraw fails verification
+            val feeWithdraw = v.denominationRecord.feeWithdraw + 
Amount.min(v.denominationRecord.feeWithdraw.currency)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(feeWithdraw = feeWithdraw),
+                    v.masterPub
+                )
+            )
+            // different feeDeposit fails verification
+            val feeDeposit = v.denominationRecord.feeDeposit + 
Amount.min(v.denominationRecord.feeDeposit.currency)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(feeDeposit = feeDeposit),
+                    v.masterPub
+                )
+            )
+            // different feeRefresh fails verification
+            val feeRefresh = v.denominationRecord.feeRefresh + 
Amount.min(v.denominationRecord.feeRefresh.currency)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(feeRefresh = feeRefresh),
+                    v.masterPub
+                )
+            )
+            // different feeRefund fails verification
+            val feeRefund = v.denominationRecord.feeRefund + 
Amount.min(v.denominationRecord.feeRefund.currency)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(feeRefund = feeRefund),
+                    v.masterPub
+                )
+            )
+            // different stampStart fails verification
+            val stampStart = Timestamp(v.denominationRecord.stampStart.ms + 
1000)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(stampStart = stampStart),
+                    v.masterPub
+                )
+            )
+            // different stampExpireWithdraw fails verification
+            val stampExpireWithdraw = 
Timestamp(v.denominationRecord.stampExpireWithdraw.ms + 1000)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(stampExpireWithdraw = 
stampExpireWithdraw),
+                    v.masterPub
+                )
+            )
+            // different stampExpireLegal fails verification
+            val stampExpireLegal = 
Timestamp(v.denominationRecord.stampExpireLegal.ms + 1000)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(stampExpireLegal = 
stampExpireLegal),
+                    v.masterPub
+                )
+            )
+            // different stampExpireDeposit fails verification
+            val stampExpireDeposit = 
Timestamp(v.denominationRecord.stampExpireDeposit.ms + 1000)
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(stampExpireDeposit = 
stampExpireDeposit),
+                    v.masterPub
+                )
+            )
+            // different masterPub fails verification
+            val size = 
Base32Crockford.calculateDecodedDataLength(v.denominationRecord.masterSig.length)
+            val masterSig = Base32Crockford.encode(Random.nextBytes(size))
+            assertFalse(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(masterSig = masterSig),
+                    v.masterPub
+                )
+            )
+            // different status does not affect verification
+            assertTrue(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(status = VerifiedBad),
+                    v.masterPub
+                )
+            )
+            // different exchangeBaseUrl does not affect verification
+            assertTrue(
+                signature.verifyDenominationRecord(
+                    v.denominationRecord.copy(exchangeBaseUrl = "foo.bar"),
+                    v.masterPub
+                )
+            )
+        }
+    }
+
+    private class WireAccountSignatureVector(val paytoUri: String, val 
signature: String, val masterPub: String)
+
+    @Test
+    fun testVerifyWireAccount() {
+        val paytoUri = "payto://x-taler-bank/localhost/Exchange"
+        val vectors = listOf(
+            WireAccountSignatureVector(
+                paytoUri,
+                
"7THNYN5G12GXZABHP187XJZ3ACTDKAWGHWYM2ERA1VGE4JMGPADXT37ZM8D4DVAQTSP8CR61VAD4ZZSWKRPP5KQ12JCTVHCKDD3KA3R",
+                "QPJSEA4SM8E67106C6MN2TMG4308J20C1T0D411WED1FJ00VF8ZG"
+            ),
+            WireAccountSignatureVector(
+                paytoUri,
+                
"6JJDJ0HG3AGRTEY1FFGHR89367GPE4FS7TC8Z26N34PHFAMSRXQ4FA7P96CDS6625SRNAN4DY1NK4TNBQXKRXQ9QR82HEVCB2FF1E18",
+                "8QBE0SRR84GWJ1FX2QCGGNRZTWA2FCV6YQC98Q26DRRJ0QBQE930"
+            ),
+            WireAccountSignatureVector(
+                paytoUri,
+                
"X5EYQJ388P8AH1PPYD2GFQ9Y1NGA7HV0TXAW6GJ7C0D6R6GAY0059HCDBE98TKJJT6MB1S660FV13DV46JDKJ622MR961XVGP6DG618",
+                "802M037TN8GHBXEGBPC7J41HJC06TWBWXYH36AYQHRJ7NVHJBQQ0"
+            )
+        )
+        for (v in vectors) {
+            // verification succeeds as expected
+            assertTrue(signature.verifyWireAccount(v.paytoUri, v.signature, 
v.masterPub))
+            // different paytoUri fails verification
+            assertFalse(signature.verifyWireAccount("foo", v.signature, 
v.masterPub))
+            // different paytoUri fails verification
+            val size = 
Base32Crockford.calculateDecodedDataLength(v.signature.length)
+            val sig = Base32Crockford.encode(Random.nextBytes(size))
+            assertFalse(signature.verifyWireAccount(v.paytoUri, sig, 
v.masterPub))
+            // different paytoUri fails verification
+            val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+            assertFalse(signature.verifyWireAccount(v.paytoUri, sig, 
masterPub))
+        }
+    }
+
 }

-- 
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]