From 054f2ab51c56a0dbb95babd5de97a7148e5af232 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Mon, 23 May 2022 21:12:31 +0200 Subject: [PATCH] -implement batch withdraw client-side logic --- src/include/taler_exchange_service.h | 224 +++++++++- src/lib/Makefile.am | 2 + src/lib/exchange_api_batch_withdraw.c | 433 +++++++++++++++++++ src/lib/exchange_api_batch_withdraw2.c | 560 +++++++++++++++++++++++++ src/lib/exchange_api_withdraw.c | 24 +- src/testing/testing_api_cmd_withdraw.c | 19 +- 6 files changed, 1235 insertions(+), 27 deletions(-) create mode 100644 src/lib/exchange_api_batch_withdraw.c create mode 100644 src/lib/exchange_api_batch_withdraw2.c diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h index ecb74bd6f..6ff9ce5b1 100644 --- a/src/include/taler_exchange_service.h +++ b/src/include/taler_exchange_service.h @@ -1882,6 +1882,29 @@ TALER_EXCHANGE_reserves_history_cancel ( struct TALER_EXCHANGE_WithdrawHandle; +/** + * Information input into the withdraw process per coin. + */ +struct TALER_EXCHANGE_WithdrawCoinInput +{ + /** + * Denomination of the coin. + */ + const struct TALER_EXCHANGE_DenomPublicKey *pk; + + /** + * Master key material for the coin. + */ + const struct TALER_PlanchetMasterSecretP *ps; + + /** + * Age commitment for the coin. + */ + const struct TALER_AgeCommitmentHash *ach; + +}; + + /** * All the details about a coin that are generated during withdrawal and that * may be needed for future operations on the coin. @@ -1988,11 +2011,8 @@ typedef void * same arguments in case of failures. * * @param exchange the exchange handle; the exchange must be ready to operate - * @param pk kind of coin to create * @param reserve_priv private key of the reserve to withdraw from - * @param ps secrets of the planchet - * caller must have committed this value to disk before the call (with @a pk) - * @param ach hash of the age commitment that should be bound to this coin. Maybe NULL. + * @param wci inputs that determine the planchet * @param res_cb the callback to call when the final result for this request is available * @param res_cb_cls closure for @a res_cb * @return NULL @@ -2002,10 +2022,8 @@ typedef void struct TALER_EXCHANGE_WithdrawHandle * TALER_EXCHANGE_withdraw ( struct TALER_EXCHANGE_Handle *exchange, - const struct TALER_EXCHANGE_DenomPublicKey *pk, const struct TALER_ReservePrivateKeyP *reserve_priv, - const struct TALER_PlanchetMasterSecretP *ps, - const struct TALER_AgeCommitmentHash *ach, + const struct TALER_EXCHANGE_WithdrawCoinInput *wci, TALER_EXCHANGE_WithdrawCallback res_cb, void *res_cb_cls); @@ -2020,6 +2038,130 @@ void TALER_EXCHANGE_withdraw_cancel (struct TALER_EXCHANGE_WithdrawHandle *wh); +/** + * @brief A /reserves/$RESERVE_PUB/batch-withdraw Handle + */ +struct TALER_EXCHANGE_BatchWithdrawHandle; + + +/** + * Details about a response for a batch withdraw request. + */ +struct TALER_EXCHANGE_BatchWithdrawResponse +{ + /** + * HTTP response data. + */ + struct TALER_EXCHANGE_HttpResponse hr; + + /** + * Details about the response. + */ + union + { + /** + * Details if the status is #MHD_HTTP_OK. + */ + struct + { + + /** + * Array of coins returned by the batch withdraw operation. + */ + struct TALER_EXCHANGE_PrivateCoinDetails *coins; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + } success; + + /** + * Details if the status is #MHD_HTTP_ACCEPTED. + */ + struct + { + /** + * Payment target that the merchant should use + * to check for its KYC status. + */ + uint64_t payment_target_uuid; + } accepted; + + /** + * Details if the status is #MHD_HTTP_CONFLICT. + */ + struct + { + /* TODO: returning full details is not implemented */ + } conflict; + + /** + * Details if the status is #MHD_HTTP_GONE. + */ + struct + { + /* TODO: returning full details is not implemented */ + } gone; + + } details; +}; + + +/** + * Callbacks of this type are used to serve the result of submitting a + * batch withdraw request to a exchange. + * + * @param cls closure + * @param wr response details + */ +typedef void +(*TALER_EXCHANGE_BatchWithdrawCallback) ( + void *cls, + const struct TALER_EXCHANGE_BatchWithdrawResponse *wr); + + +/** + * Withdraw multiple coins from the exchange using a /reserves/$RESERVE_PUB/batch-withdraw + * request. This API is typically used by a wallet to withdraw many coins from a + * reserve. + * + * Note that to ensure that no money is lost in case of hardware + * failures, the caller must have committed (most of) the arguments to + * disk before calling, and be ready to repeat the request with the + * same arguments in case of failures. + * + * @param exchange the exchange handle; the exchange must be ready to operate + * @param reserve_priv private key of the reserve to withdraw from + * @param wcis inputs that determine the planchets + * @param wci_length number of entries in @a wcis + * @param res_cb the callback to call when the final result for this request is available + * @param res_cb_cls closure for @a res_cb + * @return NULL + * if the inputs are invalid (i.e. denomination key not with this exchange). + * In this case, the callback is not called. + */ +struct TALER_EXCHANGE_BatchWithdrawHandle * +TALER_EXCHANGE_batch_withdraw ( + struct TALER_EXCHANGE_Handle *exchange, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_EXCHANGE_WithdrawCoinInput *wcis, + unsigned int wci_length, + TALER_EXCHANGE_BatchWithdrawCallback res_cb, + void *res_cb_cls); + + +/** + * Cancel a batch withdraw status request. This function cannot be used on a + * request handle if a response is already served for it. + * + * @param wh the batch withdraw handle + */ +void +TALER_EXCHANGE_batch_withdraw_cancel ( + struct TALER_EXCHANGE_BatchWithdrawHandle *wh); + + /** * Callbacks of this type are used to serve the result of submitting a * withdraw request to a exchange without the (un)blinding factor. @@ -2082,6 +2224,74 @@ void TALER_EXCHANGE_withdraw2_cancel (struct TALER_EXCHANGE_Withdraw2Handle *wh); +/** + * Callbacks of this type are used to serve the result of submitting a batch + * withdraw request to a exchange without the (un)blinding factor. + * + * @param cls closure + * @param hr HTTP response data + * @param blind_sigs array of blind signatures over the coins, NULL on error + * @param blind_sigs_length length of @a blind_sigs + */ +typedef void +(*TALER_EXCHANGE_BatchWithdraw2Callback) ( + void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_BlindedDenominationSignature *blind_sigs, + unsigned int blind_sigs_length); + + +/** + * @brief A /reserves/$RESERVE_PUB/batch-withdraw Handle, 2nd variant. + * This variant does not do the blinding/unblinding and only + * fetches the blind signatures on the already blinded planchets. + * Used internally by the `struct TALER_EXCHANGE_BatchWithdrawHandle` + * implementation as well as for the tipping logic of merchants. + */ +struct TALER_EXCHANGE_BatchWithdraw2Handle; + + +/** + * Withdraw a coin from the exchange using a /reserves/$RESERVE_PUB/batch-withdraw + * request. This API is typically used by a merchant to withdraw a tip + * where the blinding factor is unknown to the merchant. + * + * Note that to ensure that no money is lost in case of hardware + * failures, the caller must have committed (most of) the arguments to + * disk before calling, and be ready to repeat the request with the + * same arguments in case of failures. + * + * @param exchange the exchange handle; the exchange must be ready to operate + * @param pds array of planchet details of the planchet to withdraw + * @param pds_length number of entries in the @a pds array + * @param reserve_priv private key of the reserve to withdraw from + * @param res_cb the callback to call when the final result for this request is available + * @param res_cb_cls closure for @a res_cb + * @return NULL + * if the inputs are invalid (i.e. denomination key not with this exchange). + * In this case, the callback is not called. + */ +struct TALER_EXCHANGE_BatchWithdraw2Handle * +TALER_EXCHANGE_batch_withdraw2 ( + struct TALER_EXCHANGE_Handle *exchange, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_PlanchetDetail *pds, + unsigned int pds_length, + TALER_EXCHANGE_BatchWithdraw2Callback res_cb, + void *res_cb_cls); + + +/** + * Cancel a batch withdraw request. This function cannot be used + * on a request handle if a response is already served for it. + * + * @param wh the withdraw handle + */ +void +TALER_EXCHANGE_batch_withdraw2_cancel ( + struct TALER_EXCHANGE_BatchWithdraw2Handle *wh); + + /* ********************* /refresh/melt+reveal ***************************** */ diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 0ed7b1480..aa53d4493 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -22,6 +22,8 @@ libtalerexchange_la_LDFLAGS = \ -no-undefined libtalerexchange_la_SOURCES = \ exchange_api_auditor_add_denomination.c \ + exchange_api_batch_withdraw.c \ + exchange_api_batch_withdraw2.c \ exchange_api_curl_defaults.c exchange_api_curl_defaults.h \ exchange_api_common.c \ exchange_api_contracts_get.c \ diff --git a/src/lib/exchange_api_batch_withdraw.c b/src/lib/exchange_api_batch_withdraw.c new file mode 100644 index 000000000..ce5de3fc2 --- /dev/null +++ b/src/lib/exchange_api_batch_withdraw.c @@ -0,0 +1,433 @@ +/* + This file is part of TALER + Copyright (C) 2014-2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + +*/ +/** + * @file lib/exchange_api_batch_withdraw.c + * @brief Implementation of /reserves/$RESERVE_PUB/batch-withdraw requests with blinding/unblinding + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include /* just for HTTP status codes */ +#include +#include +#include +#include "taler_exchange_service.h" +#include "taler_json_lib.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * Data we keep per coin in the batch. + */ +struct CoinData +{ + + /** + * Denomination key we are withdrawing. + */ + struct TALER_EXCHANGE_DenomPublicKey pk; + + /** + * Master key material for the coin. + */ + struct TALER_PlanchetMasterSecretP ps; + + /** + * Age commitment for the coin. + */ + const struct TALER_AgeCommitmentHash *ach; + + /** + * blinding secret + */ + union TALER_DenominationBlindingKeyP bks; + + /** + * Private key of the coin we are withdrawing. + */ + struct TALER_CoinSpendPrivateKeyP priv; + + /** + * Details of the planchet. + */ + struct TALER_PlanchetDetail pd; + + /** + * Values of the @cipher selected + */ + struct TALER_ExchangeWithdrawValues alg_values; + + /** + * Hash of the public key of the coin we are signing. + */ + struct TALER_CoinPubHashP c_hash; + + /** + * Handler for the CS R request (only used for TALER_DENOMINATION_CS denominations) + */ + struct TALER_EXCHANGE_CsRWithdrawHandle *csrh; + + /** + * Batch withdraw this coin is part of. + */ + struct TALER_EXCHANGE_BatchWithdrawHandle *wh; +}; + + +/** + * @brief A batch withdraw handle + */ +struct TALER_EXCHANGE_BatchWithdrawHandle +{ + + /** + * The connection to exchange this request handle will use + */ + struct TALER_EXCHANGE_Handle *exchange; + + /** + * Handle for the actual (internal) batch withdraw operation. + */ + struct TALER_EXCHANGE_BatchWithdraw2Handle *wh2; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_BatchWithdrawCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reserve private key. + */ + const struct TALER_ReservePrivateKeyP *reserve_priv; + + /** + * Array of per-coin data. + */ + struct CoinData *coins; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + + /** + * Number of CS requests still pending. + */ + unsigned int cs_pending; + +}; + + +/** + * Function called when we're done processing the + * HTTP /reserves/$RESERVE_PUB/batch-withdraw request. + * + * @param cls the `struct TALER_EXCHANGE_BatchWithdrawHandle` + * @param hr HTTP response data + * @param blind_sig blind signature over the coin, NULL on error + */ +static void +handle_reserve_batch_withdraw_finished ( + void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_BlindedDenominationSignature *blind_sigs, + unsigned int blind_sigs_length) +{ + struct TALER_EXCHANGE_BatchWithdrawHandle *wh = cls; + struct TALER_EXCHANGE_BatchWithdrawResponse wr = { + .hr = *hr + }; + struct TALER_EXCHANGE_PrivateCoinDetails coins[wh->num_coins]; + + wh->wh2 = NULL; + if (blind_sigs_length != wh->num_coins) + { + GNUNET_break_op (0); + wr.hr.http_status = 0; + wr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + switch (hr->http_status) + { + case MHD_HTTP_OK: + { + for (unsigned int i = 0; inum_coins; i++) + { + struct CoinData *cd = &wh->coins[i]; + struct TALER_EXCHANGE_PrivateCoinDetails *coin = &coins[i]; + struct TALER_FreshCoin fc; + + if (GNUNET_OK != + TALER_planchet_to_coin (&cd->pk.key, + &blind_sigs[i], + &cd->bks, + &cd->priv, + cd->ach, + &cd->c_hash, + &cd->alg_values, + &fc)) + { + wr.hr.http_status = 0; + wr.hr.ec = TALER_EC_EXCHANGE_WITHDRAW_UNBLIND_FAILURE; + break; + } + coin->coin_priv = cd->priv; + coin->bks = cd->bks; + coin->sig = fc.sig; + coin->exchange_vals = cd->alg_values; + } + wr.details.success.coins = coins; + wr.details.success.num_coins = wh->num_coins; + break; + } + case MHD_HTTP_ACCEPTED: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("payment_target_uuid", + &wr.details.accepted.payment_target_uuid), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (hr->reply, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + wr.hr.http_status = 0; + wr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + break; + default: + break; + } + wh->cb (wh->cb_cls, + &wr); + for (unsigned int i = 0; inum_coins; i++) + TALER_denom_sig_free (&coins[i].sig); + TALER_EXCHANGE_batch_withdraw_cancel (wh); +} + + +/** + * Runs phase two, the actual withdraw operation. + * Started once the preparation for CS-denominations is + * done. + * + * @param[in,out] wh batch withdraw to start phase 2 for + */ +static void +phase_two (struct TALER_EXCHANGE_BatchWithdrawHandle *wh) +{ + struct TALER_PlanchetDetail pds[wh->num_coins]; + + for (unsigned int i = 0; inum_coins; i++) + { + struct CoinData *cd = &wh->coins[i]; + + pds[i] = cd->pd; + } + wh->wh2 = TALER_EXCHANGE_batch_withdraw2 ( + wh->exchange, + wh->reserve_priv, + pds, + wh->num_coins, + &handle_reserve_batch_withdraw_finished, + wh); +} + + +/** + * Function called when stage 1 of CS withdraw is finished (request r_pub's) + * + * @param cls the `struct CoinData *` + * @param csrr replies from the /csr-withdraw request + */ +static void +withdraw_cs_stage_two_callback ( + void *cls, + const struct TALER_EXCHANGE_CsRWithdrawResponse *csrr) +{ + struct CoinData *cd = cls; + struct TALER_EXCHANGE_BatchWithdrawHandle *wh = cd->wh; + struct TALER_EXCHANGE_BatchWithdrawResponse wr = { + .hr = csrr->hr + }; + + cd->csrh = NULL; + GNUNET_assert (TALER_DENOMINATION_CS == cd->pk.key.cipher); + switch (csrr->hr.http_status) + { + case MHD_HTTP_OK: + cd->alg_values = csrr->details.success.alg_values; + TALER_planchet_setup_coin_priv (&cd->ps, + &cd->alg_values, + &cd->priv); + TALER_planchet_blinding_secret_create (&cd->ps, + &cd->alg_values, + &cd->bks); + /* This initializes the 2nd half of the + wh->pd.blinded_planchet! */ + if (GNUNET_OK != + TALER_planchet_prepare (&cd->pk.key, + &cd->alg_values, + &cd->bks, + &cd->priv, + cd->ach, + &cd->c_hash, + &cd->pd)) + { + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw_cancel (wh); + } + wh->cs_pending--; + if (0 == wh->cs_pending) + phase_two (wh); + return; + default: + break; + } + wh->cb (wh->cb_cls, + &wr); + TALER_EXCHANGE_batch_withdraw_cancel (wh); +} + + +struct TALER_EXCHANGE_BatchWithdrawHandle * +TALER_EXCHANGE_batch_withdraw ( + struct TALER_EXCHANGE_Handle *exchange, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_EXCHANGE_WithdrawCoinInput *wcis, + unsigned int wci_length, + TALER_EXCHANGE_BatchWithdrawCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_BatchWithdrawHandle *wh; + + wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdrawHandle); + wh->exchange = exchange; + wh->cb = res_cb; + wh->cb_cls = res_cb_cls; + wh->reserve_priv = reserve_priv; + wh->num_coins = wci_length; + wh->coins = GNUNET_new_array (wh->num_coins, + struct CoinData); + for (unsigned int i = 0; icoins[i]; + const struct TALER_EXCHANGE_WithdrawCoinInput *wci = &wcis[i]; + + cd->wh = wh; + cd->ps = *wci->ps; + cd->ach = wci->ach; + cd->pk = *wci->pk; + TALER_denom_pub_deep_copy (&cd->pk.key, + &wci->pk->key); + switch (wci->pk->key.cipher) + { + case TALER_DENOMINATION_RSA: + { + cd->alg_values.cipher = TALER_DENOMINATION_RSA; + TALER_planchet_setup_coin_priv (&cd->ps, + &cd->alg_values, + &cd->priv); + TALER_planchet_blinding_secret_create (&cd->ps, + &cd->alg_values, + &cd->bks); + if (GNUNET_OK != + TALER_planchet_prepare (&cd->pk.key, + &cd->alg_values, + &cd->bks, + &cd->priv, + cd->ach, + &cd->c_hash, + &cd->pd)) + { + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw_cancel (wh); + return NULL; + } + break; + } + case TALER_DENOMINATION_CS: + { + TALER_cs_withdraw_nonce_derive ( + &cd->ps, + &cd->pd.blinded_planchet.details.cs_blinded_planchet.nonce); + /* Note that we only initialize the first half + of the blinded_planchet here; the other part + will be done after the /csr-withdraw request! */ + cd->pd.blinded_planchet.cipher = TALER_DENOMINATION_CS; + cd->csrh = TALER_EXCHANGE_csr_withdraw ( + exchange, + &cd->pk, + &cd->pd.blinded_planchet.details.cs_blinded_planchet.nonce, + &withdraw_cs_stage_two_callback, + cd); + if (NULL == cd->csrh) + { + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw_cancel (wh); + return NULL; + } + wh->cs_pending++; + break; + } + default: + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw_cancel (wh); + return NULL; + } + } + if (0 == wh->cs_pending) + phase_two (wh); + return wh; +} + + +void +TALER_EXCHANGE_batch_withdraw_cancel ( + struct TALER_EXCHANGE_BatchWithdrawHandle *wh) +{ + for (unsigned int i = 0; inum_coins; i++) + { + struct CoinData *cd = &wh->coins[i]; + + if (NULL != cd->csrh) + { + TALER_EXCHANGE_csr_withdraw_cancel (cd->csrh); + cd->csrh = NULL; + } + TALER_blinded_planchet_free (&cd->pd.blinded_planchet); + TALER_denom_pub_free (&cd->pk.key); + } + GNUNET_free (wh->coins); + if (NULL != wh->wh2) + { + TALER_EXCHANGE_batch_withdraw2_cancel (wh->wh2); + wh->wh2 = NULL; + } + GNUNET_free (wh); +} diff --git a/src/lib/exchange_api_batch_withdraw2.c b/src/lib/exchange_api_batch_withdraw2.c new file mode 100644 index 000000000..314bca0c3 --- /dev/null +++ b/src/lib/exchange_api_batch_withdraw2.c @@ -0,0 +1,560 @@ +/* + This file is part of TALER + Copyright (C) 2014-2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + +*/ +/** + * @file lib/exchange_api_batch_withdraw2.c + * @brief Implementation of /reserves/$RESERVE_PUB/batch-withdraw requests without blinding/unblinding + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include /* just for HTTP status codes */ +#include +#include +#include +#include "taler_exchange_service.h" +#include "taler_json_lib.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * @brief A batch withdraw handle + */ +struct TALER_EXCHANGE_BatchWithdraw2Handle +{ + + /** + * The connection to exchange this request handle will use + */ + struct TALER_EXCHANGE_Handle *exchange; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_BatchWithdraw2Callback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Total amount requested (value plus withdraw fee). + */ + struct TALER_Amount requested_amount; + + /** + * Public key of the reserve we are withdrawing from. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Number of coins expected. + */ + unsigned int num_coins; +}; + + +/** + * We got a 200 OK response for the /reserves/$RESERVE_PUB/batch-withdraw operation. + * Extract the coin's signature and return it to the caller. The signature we + * get from the exchange is for the blinded value. Thus, we first must + * unblind it and then should verify its validity against our coin's hash. + * + * If everything checks out, we return the unblinded signature + * to the application via the callback. + * + * @param wh operation handle + * @param json reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +reserve_batch_withdraw_ok (struct TALER_EXCHANGE_BatchWithdraw2Handle *wh, + const json_t *json) +{ + struct TALER_BlindedDenominationSignature blind_sigs[wh->num_coins]; + const json_t *ja = json_object_get (json, + "ev_sigs"); + const json_t *j; + unsigned int index; + struct TALER_EXCHANGE_HttpResponse hr = { + .reply = json, + .http_status = MHD_HTTP_OK + }; + + if ( (NULL == ja) || + (! json_is_array (ja)) || + (wh->num_coins != json_array_size (ja)) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + json_array_foreach (ja, index, j) + { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_blinded_denom_sig ("ev_sig", + &blind_sigs[index]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + for (unsigned int i = 0; icb (wh->cb_cls, + &hr, + blind_sigs, + wh->num_coins); + /* make sure callback isn't called again after return */ + wh->cb = NULL; + for (unsigned int i = 0; inum_coins; i++) + TALER_blinded_denom_sig_free (&blind_sigs[i]); + + return GNUNET_OK; +} + + +/** + * We got a 409 CONFLICT response for the /reserves/$RESERVE_PUB/batch-withdraw operation. + * Check the signatures on the batch withdraw transactions in the provided + * history and that the balances add up. We don't do anything directly + * with the information, as the JSON will be returned to the application. + * However, our job is ensuring that the exchange followed the protocol, and + * this in particular means checking all of the signatures in the history. + * + * @param wh operation handle + * @param json reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +reserve_batch_withdraw_payment_required ( + struct TALER_EXCHANGE_BatchWithdraw2Handle *wh, + const json_t *json) +{ + struct TALER_Amount balance; + struct TALER_Amount total_in_from_history; + struct TALER_Amount total_out_from_history; + json_t *history; + size_t len; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("balance", + &balance), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + history = json_object_get (json, + "history"); + if (NULL == history) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + /* go over transaction history and compute + total incoming and outgoing amounts */ + len = json_array_size (history); + { + struct TALER_EXCHANGE_ReserveHistoryEntry *rhistory; + + /* Use heap allocation as "len" may be very big and thus this may + not fit on the stack. Use "GNUNET_malloc_large" as a malicious + exchange may theoretically try to crash us by giving a history + that does not fit into our memory. */ + rhistory = GNUNET_malloc_large ( + sizeof (struct TALER_EXCHANGE_ReserveHistoryEntry) + * len); + if (NULL == rhistory) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_EXCHANGE_parse_reserve_history (wh->exchange, + history, + &wh->reserve_pub, + balance.currency, + &total_in_from_history, + &total_out_from_history, + len, + rhistory)) + { + GNUNET_break_op (0); + TALER_EXCHANGE_free_reserve_history (rhistory, + len); + return GNUNET_SYSERR; + } + TALER_EXCHANGE_free_reserve_history (rhistory, + len); + } + + /* Check that funds were really insufficient */ + if (0 >= TALER_amount_cmp (&wh->requested_amount, + &balance)) + { + /* Requested amount is smaller or equal to reported balance, + so this should not have failed. */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /reserves/$RESERVE_PUB/batch-withdraw request. + * + * @param cls the `struct TALER_EXCHANGE_BatchWithdraw2Handle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_reserve_batch_withdraw_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_BatchWithdraw2Handle *wh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_HttpResponse hr = { + .reply = j, + .http_status = (unsigned int) response_code + }; + + wh->job = NULL; + switch (response_code) + { + case 0: + hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + reserve_batch_withdraw_ok (wh, + j)) + { + GNUNET_break_op (0); + hr.http_status = 0; + hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == wh->cb); + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + return; + case MHD_HTTP_ACCEPTED: + /* only validate reply is well-formed */ + { + uint64_t ptu; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("payment_target_uuid", + &ptu), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + hr.http_status = 0; + hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + break; + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + hr.ec = TALER_JSON_get_error_code (j); + hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + GNUNET_break_op (0); + /* Nothing really to verify, exchange says one of the signatures is + invalid; as we checked them, this should never happen, we + should pass the JSON reply to the application */ + hr.ec = TALER_JSON_get_error_code (j); + hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, the exchange basically just says + that it doesn't know this reserve. Can happen if we + query before the wire transfer went through. + We should simply pass the JSON reply to the application. */ + hr.ec = TALER_JSON_get_error_code (j); + hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + /* The exchange says that the reserve has insufficient funds; + check the signatures in the history... */ + if (GNUNET_OK != + reserve_batch_withdraw_payment_required (wh, + j)) + { + GNUNET_break_op (0); + hr.http_status = 0; + hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + else + { + hr.ec = TALER_JSON_get_error_code (j); + hr.hint = TALER_JSON_get_error_hint (j); + } + break; + case MHD_HTTP_GONE: + /* could happen if denomination was revoked */ + /* Note: one might want to check /keys for revocation + signature here, alas tricky in case our /keys + is outdated => left to clients */ + hr.ec = TALER_JSON_get_error_code (j); + hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + hr.ec = TALER_JSON_get_error_code (j); + hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + hr.ec = TALER_JSON_get_error_code (j); + hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange batch withdraw\n", + (unsigned int) response_code, + (int) hr.ec); + break; + } + if (NULL != wh->cb) + { + wh->cb (wh->cb_cls, + &hr, + NULL, + 0); + wh->cb = NULL; + } + TALER_EXCHANGE_batch_withdraw2_cancel (wh); +} + + +struct TALER_EXCHANGE_BatchWithdraw2Handle * +TALER_EXCHANGE_batch_withdraw2 ( + struct TALER_EXCHANGE_Handle *exchange, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_PlanchetDetail *pds, + unsigned int pds_length, + TALER_EXCHANGE_BatchWithdraw2Callback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_BatchWithdraw2Handle *wh; + const struct TALER_EXCHANGE_Keys *keys; + const struct TALER_EXCHANGE_DenomPublicKey *dk; + struct TALER_ReserveSignatureP reserve_sig; + char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; + struct TALER_BlindedCoinHashP bch; + json_t *jc; + + keys = TALER_EXCHANGE_get_keys (exchange); + if (NULL == keys) + { + GNUNET_break (0); + return NULL; + } + wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdraw2Handle); + wh->exchange = exchange; + wh->cb = res_cb; + wh->cb_cls = res_cb_cls; + wh->num_coins = pds_length; + TALER_amount_set_zero (keys->currency, + &wh->requested_amount); + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &wh->reserve_pub.eddsa_pub); + { + char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &wh->reserve_pub, + sizeof (struct TALER_ReservePublicKeyP), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "/reserves/%s/batch-withdraw", + pub_str); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Attempting to batch-withdraw from reserve %s\n", + TALER_B2S (&wh->reserve_pub)); + wh->url = TEAH_path_to_url (exchange, + arg_str); + if (NULL == wh->url) + { + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + return NULL; + } + jc = json_array (); + GNUNET_assert (NULL != jc); + for (unsigned int i = 0; idenom_pub_hash); + if (NULL == dk) + { + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + json_decref (jc); + GNUNET_break (0); + return NULL; + } + /* Compute how much we expected to charge to the reserve */ + if (0 > + TALER_amount_add (&coin_total, + &dk->fees.withdraw, + &dk->value)) + { + /* Overflow here? Very strange, our CPU must be fried... */ + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + json_decref (jc); + return NULL; + } + if (0 > + TALER_amount_add (&wh->requested_amount, + &wh->requested_amount, + &coin_total)) + { + /* Overflow here? Very strange, our CPU must be fried... */ + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + json_decref (jc); + return NULL; + } + if (GNUNET_OK != + TALER_coin_ev_hash (&pd->blinded_planchet, + &pd->denom_pub_hash, + &bch)) + { + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + json_decref (jc); + return NULL; + } + TALER_wallet_withdraw_sign (&pd->denom_pub_hash, + &coin_total, + &bch, + reserve_priv, + &reserve_sig); + withdraw_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &pd->denom_pub_hash), + TALER_JSON_pack_blinded_planchet ("coin_ev", + &pd->blinded_planchet), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &reserve_sig)); + GNUNET_assert (NULL != withdraw_obj); + GNUNET_assert (0 == + json_array_append_new (jc, + withdraw_obj)); + } + { + CURL *eh; + struct GNUNET_CURL_Context *ctx; + json_t *req; + + req = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("planchets", + jc)); + ctx = TEAH_handle_to_context (exchange); + eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&wh->post_ctx, + eh, + req)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (req); + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + return NULL; + } + json_decref (req); + wh->job = GNUNET_CURL_job_add2 (ctx, + eh, + wh->post_ctx.headers, + &handle_reserve_batch_withdraw_finished, + wh); + } + return wh; +} + + +void +TALER_EXCHANGE_batch_withdraw2_cancel ( + struct TALER_EXCHANGE_BatchWithdraw2Handle *wh) +{ + if (NULL != wh->job) + { + GNUNET_CURL_job_cancel (wh->job); + wh->job = NULL; + } + GNUNET_free (wh->url); + TALER_curl_easy_post_finished (&wh->post_ctx); + GNUNET_free (wh); +} diff --git a/src/lib/exchange_api_withdraw.c b/src/lib/exchange_api_withdraw.c index f6a60f534..6bb579c2c 100644 --- a/src/lib/exchange_api_withdraw.c +++ b/src/lib/exchange_api_withdraw.c @@ -248,10 +248,8 @@ withdraw_cs_stage_two_callback ( struct TALER_EXCHANGE_WithdrawHandle * TALER_EXCHANGE_withdraw ( struct TALER_EXCHANGE_Handle *exchange, - const struct TALER_EXCHANGE_DenomPublicKey *pk, const struct TALER_ReservePrivateKeyP *reserve_priv, - const struct TALER_PlanchetMasterSecretP *ps, - const struct TALER_AgeCommitmentHash *ach, + const struct TALER_EXCHANGE_WithdrawCoinInput *wci, TALER_EXCHANGE_WithdrawCallback res_cb, void *res_cb_cls) { @@ -262,25 +260,25 @@ TALER_EXCHANGE_withdraw ( wh->cb = res_cb; wh->cb_cls = res_cb_cls; wh->reserve_priv = reserve_priv; - wh->ps = *ps; - wh->ach = ach; - wh->pk = *pk; + wh->ps = *wci->ps; + wh->ach = wci->ach; + wh->pk = *wci->pk; TALER_denom_pub_deep_copy (&wh->pk.key, - &pk->key); + &wci->pk->key); - switch (pk->key.cipher) + switch (wci->pk->key.cipher) { case TALER_DENOMINATION_RSA: { wh->alg_values.cipher = TALER_DENOMINATION_RSA; - TALER_planchet_setup_coin_priv (ps, + TALER_planchet_setup_coin_priv (&wh->ps, &wh->alg_values, &wh->priv); - TALER_planchet_blinding_secret_create (ps, + TALER_planchet_blinding_secret_create (&wh->ps, &wh->alg_values, &wh->bks); if (GNUNET_OK != - TALER_planchet_prepare (&pk->key, + TALER_planchet_prepare (&wh->pk.key, &wh->alg_values, &wh->bks, &wh->priv, @@ -302,7 +300,7 @@ TALER_EXCHANGE_withdraw ( case TALER_DENOMINATION_CS: { TALER_cs_withdraw_nonce_derive ( - ps, + &wh->ps, &wh->pd.blinded_planchet.details.cs_blinded_planchet.nonce); /* Note that we only initialize the first half of the blinded_planchet here; the other part @@ -310,7 +308,7 @@ TALER_EXCHANGE_withdraw ( wh->pd.blinded_planchet.cipher = TALER_DENOMINATION_CS; wh->csrh = TALER_EXCHANGE_csr_withdraw ( exchange, - pk, + &wh->pk, &wh->pd.blinded_planchet.details.cs_blinded_planchet.nonce, &withdraw_cs_stage_two_callback, wh); diff --git a/src/testing/testing_api_cmd_withdraw.c b/src/testing/testing_api_cmd_withdraw.c index de862f91a..6f8b3a638 100644 --- a/src/testing/testing_api_cmd_withdraw.c +++ b/src/testing/testing_api_cmd_withdraw.c @@ -433,13 +433,18 @@ withdraw_run (void *cls, &ws->amount, &ws->pk->fees.withdraw)); ws->reserve_history.details.withdraw.fee = ws->pk->fees.withdraw; - ws->wsh = TALER_EXCHANGE_withdraw (is->exchange, - ws->pk, - rp, - &ws->ps, - ws->h_age_commitment, - &reserve_withdraw_cb, - ws); + { + struct TALER_EXCHANGE_WithdrawCoinInput wci = { + .pk = ws->pk, + .ps = &ws->ps, + .ach = ws->h_age_commitment + }; + ws->wsh = TALER_EXCHANGE_withdraw (is->exchange, + rp, + &wci, + &reserve_withdraw_cb, + ws); + } if (NULL == ws->wsh) { GNUNET_break (0);