exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 2a5d98f79cec4fe444050974008a0a36647e257a
parent 2dec7c53130e2511481fb0246776422e6e6f474f
Author: Özgür Kesim <oec@codeblau.de>
Date:   Mon, 14 Sep 2026 20:14:58 +0200

util: batch recoup confirmation signatures

Add TALER_RecoupWithdrawBatchConfirmationPS and
TALER_RecoupRefreshBatchConfirmationPS with sign/verify helpers,
the struct TALER_RecoupedCoin and TALER_recoup_batch_hash()
computing the hash over the recouped coins that both signatures cover,
as specified for the batch recoup protocol (#9828).

test_crypto covers hash order sensitivity and positive/negative
verification of both signatures.

Diffstat:
Msrc/include/taler/taler_crypto_lib.h | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/exchange_signatures.c | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/test_crypto.c | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 569 insertions(+), 0 deletions(-)

diff --git a/src/include/taler/taler_crypto_lib.h b/src/include/taler/taler_crypto_lib.h @@ -5817,6 +5817,142 @@ TALER_exchange_online_confirm_recoup_refresh_verify ( /** + * Details about one coin recouped in a batch recoup operation + * (``/recoup-withdraw`` or ``/recoup-refresh``). + */ +struct TALER_RecoupedCoin +{ + /** + * Public key of the recouped coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Amount credited for this coin, its residual value at the + * time of the recoup. + */ + struct TALER_Amount amount; +}; + + +/** + * Compute the hash over the coins recouped in one batch recoup + * operation, as used in the batch confirmation signatures. + * The hash is over the concatenation, in order, of the coin public + * key and the amount (in network byte order) of each entry. + * + * @param num_recoups number of entries in @a recoups + * @param recoups the recouped coins, in the order of the response + * @param[out] h_recoups set to the resulting hash + */ +void +TALER_recoup_batch_hash ( + size_t num_recoups, + const struct TALER_RecoupedCoin recoups[static num_recoups], + struct GNUNET_HashCode *h_recoups); + + +/** + * Create the signature confirming a batch recoup to a reserve + * (response to ``/recoup-withdraw``). + * + * @param scb function to call to create the signature + * @param timestamp when was the recoup done + * @param reserve_pub reserve that was credited + * @param planchets_h commitment of the withdraw operation the coins came from + * @param total_amount total amount credited to the reserve + * @param h_recoups hash over the recouped coins, see #TALER_recoup_batch_hash() + * @param[out] pub where to write the public key + * @param[out] sig where to write the signature + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TALER_exchange_online_confirm_recoup_withdraw_batch_sign ( + TALER_ExchangeSignCallback scb, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_HashBlindedPlanchetsP *planchets_h, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig); + + +/** + * Verify the signature confirming a batch recoup to a reserve + * (response to ``/recoup-withdraw``). + * + * @param timestamp when was the recoup done + * @param reserve_pub reserve that was credited + * @param planchets_h commitment of the withdraw operation the coins came from + * @param total_amount total amount credited to the reserve + * @param h_recoups hash over the recouped coins, see #TALER_recoup_batch_hash() + * @param pub public key used to create @a sig + * @param sig the signature + * @return #GNUNET_OK if the signature is valid + */ +enum GNUNET_GenericReturnValue +TALER_exchange_online_confirm_recoup_withdraw_batch_verify ( + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_HashBlindedPlanchetsP *planchets_h, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + const struct TALER_ExchangePublicKeyP *pub, + const struct TALER_ExchangeSignatureP *sig); + + +/** + * Create the signature confirming a batch recoup to an old coin + * (response to ``/recoup-refresh``). + * + * @param scb function to call to create the signature + * @param timestamp when was the recoup done + * @param old_coin_pub old coin that was credited + * @param rc commitment of the refresh operation the coins came from + * @param total_amount total amount credited to the old coin + * @param h_recoups hash over the recouped coins, see #TALER_recoup_batch_hash() + * @param[out] pub where to write the public key + * @param[out] sig where to write the signature + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TALER_exchange_online_confirm_recoup_refresh_batch_sign ( + TALER_ExchangeSignCallback scb, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *old_coin_pub, + const struct TALER_RefreshCommitmentP *rc, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig); + + +/** + * Verify the signature confirming a batch recoup to an old coin + * (response to ``/recoup-refresh``). + * + * @param timestamp when was the recoup done + * @param old_coin_pub old coin that was credited + * @param rc commitment of the refresh operation the coins came from + * @param total_amount total amount credited to the old coin + * @param h_recoups hash over the recouped coins, see #TALER_recoup_batch_hash() + * @param pub public key used to create @a sig + * @param sig the signature + * @return #GNUNET_OK if the signature is valid + */ +enum GNUNET_GenericReturnValue +TALER_exchange_online_confirm_recoup_refresh_batch_verify ( + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *old_coin_pub, + const struct TALER_RefreshCommitmentP *rc, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + const struct TALER_ExchangePublicKeyP *pub, + const struct TALER_ExchangeSignatureP *sig); + + +/** * Create denomination unknown signature. * * @param scb function to call to create the signature diff --git a/src/util/exchange_signatures.c b/src/util/exchange_signatures.c @@ -1062,6 +1062,241 @@ TALER_exchange_online_confirm_recoup_refresh_verify ( } +void +TALER_recoup_batch_hash ( + size_t num_recoups, + const struct TALER_RecoupedCoin recoups[static num_recoups], + struct GNUNET_HashCode *h_recoups) +{ + struct GNUNET_HashContext *hc; + + hc = GNUNET_CRYPTO_hash_context_start (); + for (size_t i = 0; i < num_recoups; i++) + { + struct TALER_AmountNBO amount_nbo; + + GNUNET_CRYPTO_hash_context_read (hc, + &recoups[i].coin_pub, + sizeof (recoups[i].coin_pub)); + TALER_amount_hton (&amount_nbo, + &recoups[i].amount); + GNUNET_CRYPTO_hash_context_read (hc, + &amount_nbo, + sizeof (amount_nbo)); + } + GNUNET_CRYPTO_hash_context_finish (hc, + h_recoups); +} + + +GNUNET_NETWORK_STRUCT_BEGIN + +/** + * Response by which the exchange affirms that it credited a reserve + * for all coins recouped in one /recoup-withdraw request. + */ +struct TALER_RecoupWithdrawBatchConfirmationPS +{ + + /** + * Purpose is #TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_WITHDRAW_BATCH + */ + struct GNUNET_CRYPTO_SignaturePurpose purpose; + + /** + * When did the exchange accept the recoup request? + */ + struct GNUNET_TIME_TimestampNBO timestamp; + + /** + * Public key of the reserve that was credited. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Commitment of the withdraw operation the coins originated from. + */ + struct TALER_HashBlindedPlanchetsP planchets_h; + + /** + * Total amount credited to the reserve. + */ + struct TALER_AmountNBO total_amount; + + /** + * Hash over the recouped coins and their amounts, + * see #TALER_recoup_batch_hash(). + */ + struct GNUNET_HashCode h_recoups; +}; + +GNUNET_NETWORK_STRUCT_END + + +enum TALER_ErrorCode +TALER_exchange_online_confirm_recoup_withdraw_batch_sign ( + TALER_ExchangeSignCallback scb, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_HashBlindedPlanchetsP *planchets_h, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig) +{ + struct TALER_RecoupWithdrawBatchConfirmationPS pc = { + .purpose.size = htonl (sizeof (pc)), + .purpose.purpose = htonl ( + TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_WITHDRAW_BATCH), + .timestamp = GNUNET_TIME_timestamp_hton (timestamp), + .reserve_pub = *reserve_pub, + .planchets_h = *planchets_h, + .h_recoups = *h_recoups + }; + + TALER_amount_hton (&pc.total_amount, + total_amount); + return scb (&pc.purpose, + pub, + sig); +} + + +enum GNUNET_GenericReturnValue +TALER_exchange_online_confirm_recoup_withdraw_batch_verify ( + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_HashBlindedPlanchetsP *planchets_h, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + const struct TALER_ExchangePublicKeyP *pub, + const struct TALER_ExchangeSignatureP *sig) +{ + struct TALER_RecoupWithdrawBatchConfirmationPS pc = { + .purpose.size = htonl (sizeof (pc)), + .purpose.purpose = htonl ( + TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_WITHDRAW_BATCH), + .timestamp = GNUNET_TIME_timestamp_hton (timestamp), + .reserve_pub = *reserve_pub, + .planchets_h = *planchets_h, + .h_recoups = *h_recoups + }; + + TALER_amount_hton (&pc.total_amount, + total_amount); + return + GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_WITHDRAW_BATCH, + &pc, + &sig->eddsa_signature, + &pub->eddsa_pub); +} + + +GNUNET_NETWORK_STRUCT_BEGIN + +/** + * Response by which the exchange affirms that it credited an old coin + * for all coins recouped in one /recoup-refresh request. + */ +struct TALER_RecoupRefreshBatchConfirmationPS +{ + + /** + * Purpose is #TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH_BATCH + */ + struct GNUNET_CRYPTO_SignaturePurpose purpose; + + /** + * When did the exchange accept the recoup request? + */ + struct GNUNET_TIME_TimestampNBO timestamp; + + /** + * Public key of the old coin that was credited. + */ + struct TALER_CoinSpendPublicKeyP old_coin_pub; + + /** + * Commitment of the refresh operation the coins originated from. + */ + struct TALER_RefreshCommitmentP rc; + + /** + * Total amount credited to the old coin. + */ + struct TALER_AmountNBO total_amount; + + /** + * Hash over the recouped coins and their amounts, + * see #TALER_recoup_batch_hash(). + */ + struct GNUNET_HashCode h_recoups; +}; + +GNUNET_NETWORK_STRUCT_END + + +enum TALER_ErrorCode +TALER_exchange_online_confirm_recoup_refresh_batch_sign ( + TALER_ExchangeSignCallback scb, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *old_coin_pub, + const struct TALER_RefreshCommitmentP *rc, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig) +{ + struct TALER_RecoupRefreshBatchConfirmationPS pc = { + .purpose.size = htonl (sizeof (pc)), + .purpose.purpose = htonl ( + TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH_BATCH), + .timestamp = GNUNET_TIME_timestamp_hton (timestamp), + .old_coin_pub = *old_coin_pub, + .rc = *rc, + .h_recoups = *h_recoups + }; + + TALER_amount_hton (&pc.total_amount, + total_amount); + return scb (&pc.purpose, + pub, + sig); +} + + +enum GNUNET_GenericReturnValue +TALER_exchange_online_confirm_recoup_refresh_batch_verify ( + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *old_coin_pub, + const struct TALER_RefreshCommitmentP *rc, + const struct TALER_Amount *total_amount, + const struct GNUNET_HashCode *h_recoups, + const struct TALER_ExchangePublicKeyP *pub, + const struct TALER_ExchangeSignatureP *sig) +{ + struct TALER_RecoupRefreshBatchConfirmationPS pc = { + .purpose.size = htonl (sizeof (pc)), + .purpose.purpose = htonl ( + TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH_BATCH), + .timestamp = GNUNET_TIME_timestamp_hton (timestamp), + .old_coin_pub = *old_coin_pub, + .rc = *rc, + .h_recoups = *h_recoups + }; + + TALER_amount_hton (&pc.total_amount, + total_amount); + return + GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH_BATCH, + &pc, + &sig->eddsa_signature, + &pub->eddsa_pub); +} + + GNUNET_NETWORK_STRUCT_BEGIN /** diff --git a/src/util/test_crypto.c b/src/util/test_crypto.c @@ -399,6 +399,202 @@ test_exchange_sigs (void) } +/** + * Private key used by #test_sign_cb(). + */ +static struct TALER_ExchangePrivateKeyP test_exchange_priv; + + +/** + * Sign @a purpose with #test_exchange_priv, standing in for + * the exchange's signing helper. + * + * @param purpose message to sign + * @param[out] pub set to the public key of #test_exchange_priv + * @param[out] sig set to the signature + * @return #TALER_EC_NONE + */ +static enum TALER_ErrorCode +test_sign_cb (const struct GNUNET_CRYPTO_SignaturePurpose *purpose, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig) +{ + GNUNET_CRYPTO_eddsa_key_get_public (&test_exchange_priv.eddsa_priv, + &pub->eddsa_pub); + GNUNET_CRYPTO_eddsa_sign_ (&test_exchange_priv.eddsa_priv, + purpose, + &sig->eddsa_signature); + return TALER_EC_NONE; +} + + +/** + * Test the batch recoup confirmation signatures and the hash + * over the recouped coins they cover. + * + * @return 0 on success + */ +static int +test_recoup_batch_sigs (void) +{ + struct TALER_RecoupedCoin recoups[3]; + struct TALER_RecoupedCoin swapped[3]; + struct GNUNET_HashCode h_recoups; + struct GNUNET_HashCode h_swapped; + struct TALER_Amount total; + struct TALER_ReservePublicKeyP reserve_pub; + struct TALER_HashBlindedPlanchetsP planchets_h; + struct TALER_CoinSpendPublicKeyP old_coin_pub; + struct TALER_RefreshCommitmentP rc; + struct GNUNET_TIME_Timestamp ts; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + GNUNET_CRYPTO_eddsa_key_create (&test_exchange_priv.eddsa_priv); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ("EUR", + &total)); + for (unsigned int i = 0; i < 3; i++) + { + GNUNET_CRYPTO_random_block (&recoups[i].coin_pub, + sizeof (recoups[i].coin_pub)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("EUR:1.5", + &recoups[i].amount)); + GNUNET_assert (0 <= + TALER_amount_add (&total, + &total, + &recoups[i].amount)); + } + GNUNET_CRYPTO_random_block (&reserve_pub, + sizeof (reserve_pub)); + GNUNET_CRYPTO_random_block (&planchets_h, + sizeof (planchets_h)); + GNUNET_CRYPTO_random_block (&old_coin_pub, + sizeof (old_coin_pub)); + GNUNET_CRYPTO_random_block (&rc, + sizeof (rc)); + ts = GNUNET_TIME_timestamp_get (); + + /* the hash must depend on the order of the coins */ + TALER_recoup_batch_hash (3, + recoups, + &h_recoups); + swapped[0] = recoups[1]; + swapped[1] = recoups[0]; + swapped[2] = recoups[2]; + TALER_recoup_batch_hash (3, + swapped, + &h_swapped); + if (0 == + GNUNET_memcmp (&h_recoups, + &h_swapped)) + { + GNUNET_break (0); + return 1; + } + + /* withdraw batch confirmation */ + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_withdraw_batch_sign ( + &test_sign_cb, + ts, + &reserve_pub, + &planchets_h, + &total, + &h_recoups, + &pub, + &sig)) + { + GNUNET_break (0); + return 1; + } + if (GNUNET_OK != + TALER_exchange_online_confirm_recoup_withdraw_batch_verify ( + ts, + &reserve_pub, + &planchets_h, + &total, + &h_recoups, + &pub, + &sig)) + { + GNUNET_break (0); + return 1; + } + if (GNUNET_OK == + TALER_exchange_online_confirm_recoup_withdraw_batch_verify ( + ts, + &reserve_pub, + &planchets_h, + &total, + &h_swapped, + &pub, + &sig)) + { + GNUNET_break (0); + return 1; + } + if (GNUNET_OK == + TALER_exchange_online_confirm_recoup_withdraw_batch_verify ( + ts, + &reserve_pub, + &planchets_h, + &recoups[0].amount, + &h_recoups, + &pub, + &sig)) + { + GNUNET_break (0); + return 1; + } + + /* refresh batch confirmation */ + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_refresh_batch_sign ( + &test_sign_cb, + ts, + &old_coin_pub, + &rc, + &total, + &h_recoups, + &pub, + &sig)) + { + GNUNET_break (0); + return 1; + } + if (GNUNET_OK != + TALER_exchange_online_confirm_recoup_refresh_batch_verify ( + ts, + &old_coin_pub, + &rc, + &total, + &h_recoups, + &pub, + &sig)) + { + GNUNET_break (0); + return 1; + } + if (GNUNET_OK == + TALER_exchange_online_confirm_recoup_refresh_batch_verify ( + GNUNET_TIME_timestamp_get (), + &old_coin_pub, + &rc, + &total, + &h_recoups, + &pub, + &sig)) + { + /* only fails if the clock did not advance; tolerate that */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "timestamp did not change, skipping negative check\n"); + } + return 0; +} + + static int test_merchant_sigs (void) { @@ -556,6 +752,8 @@ main (int argc, return 3; if (0 != test_exchange_sigs ()) return 4; + if (0 != test_recoup_batch_sigs ()) + return 9; if (0 != test_merchant_sigs ()) return 5; if (0 != test_contracts ())