diff options
author | Torsten Grote <t@grobox.de> | 2020-06-29 14:59:13 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-06-29 15:05:46 -0300 |
commit | cd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9 (patch) | |
tree | 80cd3fd96fef926f929fb8847bee439e85befa77 | |
parent | 86bd04302b8691aa3e518a70bafa9d95d8358e82 (diff) | |
download | wallet-kotlin-cd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9.tar.gz wallet-kotlin-cd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9.tar.bz2 wallet-kotlin-cd46828dd54c9dc17e0c5f5fe0817d1407cc8bd9.zip |
Add verification methods for various signatures
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)) + } + } + } |