summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-06-29 14:59:13 -0300
committerTorsten Grote <t@grobox.de>2020-06-29 15:05:46 -0300
commitcd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9 (patch)
tree80cd3fd96fef926f929fb8847bee439e85befa77
parent86bd04302b8691aa3e518a70bafa9d95d8358e82 (diff)
downloadwallet-kotlin-cd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9.tar.gz
wallet-kotlin-cd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9.tar.bz2
wallet-kotlin-cd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9.zip
Add verification methods for various signatures
-rw-r--r--.idea/dictionaries/user.xml1
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt2
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt114
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt5
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt95
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt309
6 files changed, 523 insertions, 3 deletions
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..c8aa990
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
@@ -0,0 +1,114 @@
+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
+}
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/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/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))
+ }
+ }
+
}