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