exchange

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

commit 4a83e23f67e2f5ca7acc242137e2bea94d6c4c00
parent 17c00ab24e33118d269e3725c02501322b09de06
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue,  3 Mar 2026 23:26:21 +0100

split off post-withdraw and post-withdraw_blinded

Diffstat:
Msrc/include/taler/taler-exchange/Makefile.am | 3++-
Msrc/include/taler/taler-exchange/post-withdraw.h | 311-------------------------------------------------------------------------------
Msrc/include/taler/taler_exchange_service.h | 1+
Msrc/lib/Makefile.am | 1+
Asrc/lib/exchange_api_get-keys.c | 554+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/exchange_api_post-withdraw.c | 805-------------------------------------------------------------------------------
Asrc/lib/exchange_api_post-withdraw_blinded.c | 840+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 1398 insertions(+), 1117 deletions(-)

diff --git a/src/include/taler/taler-exchange/Makefile.am b/src/include/taler/taler-exchange/Makefile.am @@ -55,4 +55,5 @@ talerinclude_HEADERS = \ post-reserves-RESERVE_PUB-purse.h \ post-reveal-melt.h \ post-reveal-withdraw.h \ - post-withdraw.h + post-withdraw.h \ + post-withdraw_blinded.h diff --git a/src/include/taler/taler-exchange/post-withdraw.h b/src/include/taler/taler-exchange/post-withdraw.h @@ -426,315 +426,4 @@ TALER_EXCHANGE_post_withdraw_cancel ( struct TALER_EXCHANGE_PostWithdrawHandle *pwh); -/* *** Low-level withdraw (with pre-blinded planchets) *** */ - - -/** - * @brief Information needed to withdraw coins (low-level, pre-blinded). - */ -struct TALER_EXCHANGE_WithdrawBlindedCoinInput -{ - /** - * The denomination of the coin. - */ - const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; - - /** - * Blinded planchet for a single coin. - */ - struct TALER_PlanchetDetail planchet_details; -}; - -/** - * @brief Information needed to withdraw age-restricted coins (low-level, pre-blinded). - */ -struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput -{ - /** - * The denomination of the coin. MUST support age restriction. - */ - const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; - - /** - * Tuple of length kappa of planchet candidates for a single coin. - */ - struct TALER_PlanchetDetail planchet_details[TALER_CNC_KAPPA]; -}; - - -/** - * Handle for an operation to POST /withdraw (low-level, pre-blinded variant). - */ -struct TALER_EXCHANGE_PostWithdrawBlindedHandle; - - -/** - * Set up POST /withdraw operation (low-level variant with pre-blinded - * planchets). - * Note that you must explicitly start the operation after setup. - * - * This variant does not do the blinding/unblinding and only - * fetches the blind signatures on the already blinded planchets. - * - * For age-restricted coins requiring a proof, pass @a blinded_input as NULL - * and supply the age-restricted input via the - * #TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF option. - * - * @param curl_ctx The curl context to use - * @param keys The /keys material from the exchange - * @param exchange_url The base-URL of the exchange - * @param reserve_priv private key of the reserve to withdraw from - * @param blinding_seed seed used for blinding of CS denominations, might be NULL - * @param num_input number of entries in the @a blinded_input array - * @param blinded_input array of planchet details to withdraw, or NULL if using WITH_AGE_PROOF option - * @return handle to operation, NULL on error - */ -struct TALER_EXCHANGE_PostWithdrawBlindedHandle * -TALER_EXCHANGE_post_withdraw_blinded_create ( - struct GNUNET_CURL_Context *curl_ctx, - struct TALER_EXCHANGE_Keys *keys, - const char *exchange_url, - const struct TALER_ReservePrivateKeyP *reserve_priv, - const struct TALER_BlindingMasterSeedP *blinding_seed, - size_t num_input, - const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *blinded_input); - - -/** - * Possible options we can set for the POST /withdraw request - * (low-level, pre-blinded variant). - */ -enum TALER_EXCHANGE_PostWithdrawBlindedOption -{ - /** - * End of list of options. - */ - TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_END = 0, - - /** - * Upgrade to an age-proof withdrawal for age-restricted coins, requiring - * an additional call to POST /reveal-withdraw. - * The @e details.with_age_proof.max_age field gives the maximum age to - * (provably) commit to. - * The @e details.with_age_proof.input field gives the KAPPA planchet - * candidates per coin. - */ - TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF - -}; - - -/** - * Value for an option we can set for the POST /withdraw request - * (low-level, pre-blinded variant). - */ -struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue -{ - /** - * Type of the option being set. - */ - enum TALER_EXCHANGE_PostWithdrawBlindedOption option; - - /** - * Specific option value. - */ - union - { - /** - * Value if @e option is - * #TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF. - */ - struct - { - /** - * The maximum age to commit to. - */ - uint8_t max_age; - - /** - * Array of KAPPA planchet candidates per coin, length matches - * the @e num_input given to _create(). - */ - const struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput *input; - - } with_age_proof; - - } details; - -}; - - -/** - * Terminate the list of options. - * - * @return the terminating object - */ -#define TALER_EXCHANGE_post_withdraw_blinded_option_end_() \ - (const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue) \ - { \ - .option = TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_END \ - } - -/** - * Upgrade to an age-proof withdrawal for age-restricted coins. - * - * @param age the maximum age to commit to - * @param inp pointer to array of KAPPA planchet candidates per coin - * @return representation of the option - */ -#define TALER_EXCHANGE_post_withdraw_blinded_option_with_age_proof(age, inp) \ - (const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue) \ - { \ - .option = TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF, \ - .details.with_age_proof.max_age = (age), \ - .details.with_age_proof.input = (inp) \ - } - - -/** - * Set the requested options for the operation. - * - * If any option fails, other options may or may not be applied. - * - * @param pwbh the request to set the options for - * @param num_options length of the @a options array - * @param options an array of options - * @return #GNUNET_OK on success, - * #GNUNET_NO on failure, - * #GNUNET_SYSERR on internal error - */ -enum GNUNET_GenericReturnValue -TALER_EXCHANGE_post_withdraw_blinded_set_options_ ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh, - unsigned int num_options, - const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue options[]); - - -/** - * Set the requested options for the operation. - * - * If any option fails, other options may or may not be applied. - * - * @param pwbh the request to set the options for - * @param ... the list of the options, each created by a - * TALER_EXCHANGE_post_withdraw_blinded_option_NAME(VALUE) macro - * @return #GNUNET_OK on success, - * #GNUNET_NO on failure, - * #GNUNET_SYSERR on internal error - */ -#define TALER_EXCHANGE_post_withdraw_blinded_set_options(pwbh,...) \ - TALER_EXCHANGE_post_withdraw_blinded_set_options_ ( \ - pwbh, \ - TALER_EXCHANGE_COMMON_OPTIONS_ARRAY_MAX_SIZE, \ - ((const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue[]) \ - {__VA_ARGS__, \ - TALER_EXCHANGE_post_withdraw_blinded_option_end_ ()} \ - )) - - -/** - * Response from a POST /withdraw request (low-level, pre-blinded variant). - */ -struct TALER_EXCHANGE_PostWithdrawBlindedResponse -{ - /** - * HTTP response data. - */ - struct TALER_EXCHANGE_HttpResponse hr; - - /** - * Details about the response - */ - union - { - /** - * Details if the status is #MHD_HTTP_OK - */ - struct - { - /** - * Number of signatures returned. - */ - unsigned int num_sigs; - - /** - * Array of @e num_sigs blinded denomination signatures, giving each - * coin its value and validity. The array gives these coins in the same - * order (and should have the same length) in which the original - * withdraw request specified the respective denomination keys. - */ - const struct TALER_BlindedDenominationSignature *blinded_denom_sigs; - - /** - * The commitment of the withdraw request, needed for the later calls to /recoup - */ - struct TALER_HashBlindedPlanchetsP planchets_h; - - } ok; - - /** - * Details if the status is MHD_HTTP_CREATED, i.e. in case of - * age-restriction. The response is input to prepare the required - * follow-up call to /reveal-withdraw. - */ - struct TALER_EXCHANGE_WithdrawCreated created; - - /** - * Details if the status is #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS. - */ - struct TALER_EXCHANGE_KycNeededRedirect unavailable_for_legal_reasons; - - } details; -}; - - -#ifndef TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE -/** - * Type of the closure used by - * the #TALER_EXCHANGE_PostWithdrawBlindedCallback. - */ -#define TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE void -#endif /* TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE */ - -/** - * Type of the function that receives the result of a - * POST /withdraw request (low-level, pre-blinded variant). - * - * @param cls closure - * @param result result returned by the HTTP server - */ -typedef void -(*TALER_EXCHANGE_PostWithdrawBlindedCallback)( - TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE *cls, - const struct TALER_EXCHANGE_PostWithdrawBlindedResponse *result); - - -/** - * Start POST /withdraw operation (low-level, pre-blinded variant). - * - * @param[in,out] pwbh operation to start - * @param cb function to call with the exchange's result - * @param cb_cls closure for @a cb - * @return status code, #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TALER_EXCHANGE_post_withdraw_blinded_start ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh, - TALER_EXCHANGE_PostWithdrawBlindedCallback cb, - TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE *cb_cls); - - -/** - * Cancel POST /withdraw operation (low-level, pre-blinded variant). This - * function must not be called by clients after the - * TALER_EXCHANGE_PostWithdrawBlindedCallback has been invoked (as in those - * cases it'll be called internally by the implementation already). - * - * @param[in] pwbh operation to cancel - */ -void -TALER_EXCHANGE_post_withdraw_blinded_cancel ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh); - - #endif /* _TALER_EXCHANGE__POST_WITHDRAW_H */ diff --git a/src/include/taler/taler_exchange_service.h b/src/include/taler/taler_exchange_service.h @@ -39,6 +39,7 @@ #include <taler/taler-exchange/get-reserves-RESERVE_PUB-history.h> #include <taler/taler-exchange/post-blinding-prepare.h> #include <taler/taler-exchange/post-withdraw.h> +#include <taler/taler-exchange/post-withdraw_blinded.h> #include <taler/taler-exchange/post-reveal-withdraw.h> #include <taler/taler-exchange/post-reveal-melt.h> #include <taler/taler-exchange/post-melt.h> diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -76,6 +76,7 @@ libtalerexchange_la_SOURCES = \ exchange_api_post-reveal-melt.c \ exchange_api_post-reveal-withdraw.c \ exchange_api_post-withdraw.c \ + exchange_api_post-withdraw_blinded.c \ exchange_api_refresh_common.c exchange_api_refresh_common.h \ exchange_api_restrictions.c \ exchange_api_stefan.c diff --git a/src/lib/exchange_api_get-keys.c b/src/lib/exchange_api_get-keys.c @@ -0,0 +1,554 @@ +/* + This file is part of TALER + Copyright (C) 2014-2026 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_get-keys.c + * @brief Implementation of GET /keys + * @author Sree Harsha Totakura <sreeharsha@totakura.in> + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <microhttpd.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_exchange_service.h" +#include "taler/taler_signatures.h" +#include "exchange_api_handle.h" +#include "exchange_api_curl_defaults.h" +#include "taler/taler_curl_lib.h" + +/** + * If the "Expire" cache control header is missing, for + * how long do we assume the reply to be valid at least? + */ +#define DEFAULT_EXPIRATION GNUNET_TIME_UNIT_HOURS + +/** + * If the "Expire" cache control header is missing, for + * how long do we assume the reply to be valid at least? + */ +#define MINIMUM_EXPIRATION GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 2) + +/** + * Define a max length for the HTTP "Expire:" header + */ +#define MAX_DATE_LINE_LEN 32 + + +/** + * Handle for a GET /keys request. + */ +struct TALER_EXCHANGE_GetKeysHandle +{ + + /** + * The exchange base URL (i.e. "https://exchange.demo.taler.net/") + */ + char *exchange_url; + + /** + * The url for the /keys request, set during _start. + */ + char *url; + + /** + * Previous /keys response, NULL for none. + */ + struct TALER_EXCHANGE_Keys *prev_keys; + + /** + * Entry for this request with the `struct GNUNET_CURL_Context`. + */ + struct GNUNET_CURL_Job *job; + + /** + * Expiration time according to "Expire:" header. + * 0 if not provided by the server. + */ + struct GNUNET_TIME_Timestamp expire; + + /** + * Function to call with the exchange's certification data, + * NULL if this has already been done. + */ + TALER_EXCHANGE_GetKeysCallback cert_cb; + + /** + * Closure to pass to @e cert_cb. + */ + TALER_EXCHANGE_GET_KEYS_RESULT_CLOSURE *cert_cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + +}; + + +/** + * Parse HTTP timestamp. + * + * @param dateline header to parse header + * @param[out] at where to write the result + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_date_string (const char *dateline, + struct GNUNET_TIME_Timestamp *at) +{ + static const char *MONTHS[] = + { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", NULL }; + int year; + int mon; + int day; + int hour; + int min; + int sec; + char month[4]; + struct tm tm; + time_t t; + + /* We recognize the three formats in RFC2616, section 3.3.1. Month + names are always in English. The formats are: + Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 + Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 + Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + Note that the first is preferred. + */ + + if (strlen (dateline) > MAX_DATE_LINE_LEN) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + while (*dateline == ' ') + ++dateline; + while (*dateline && *dateline != ' ') + ++dateline; + while (*dateline == ' ') + ++dateline; + /* We just skipped over the day of the week. Now we have:*/ + if ( (sscanf (dateline, + "%d %3s %d %d:%d:%d", + &day, month, &year, &hour, &min, &sec) != 6) && + (sscanf (dateline, + "%d-%3s-%d %d:%d:%d", + &day, month, &year, &hour, &min, &sec) != 6) && + (sscanf (dateline, + "%3s %d %d:%d:%d %d", + month, &day, &hour, &min, &sec, &year) != 6) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* Two digit dates are defined to be relative to 1900; all other dates + * are supposed to be represented as four digits. */ + if (year < 100) + year += 1900; + + for (mon = 0; ; mon++) + { + if (! MONTHS[mon]) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == strcasecmp (month, + MONTHS[mon])) + break; + } + + memset (&tm, 0, sizeof(tm)); + tm.tm_year = year - 1900; + tm.tm_mon = mon; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + + t = mktime (&tm); + if (((time_t) -1) == t) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "mktime"); + return GNUNET_SYSERR; + } + if (t < 0) + t = 0; /* can happen due to timezone issues if date was 1.1.1970 */ + *at = GNUNET_TIME_timestamp_from_s (t); + return GNUNET_OK; +} + + +/** + * Function called for each header in the HTTP /keys response. + * Finds the "Expire:" header and parses it, storing the result + * in the "expire" field of the keys request. + * + * @param buffer header data received + * @param size size of an item in @a buffer + * @param nitems number of items in @a buffer + * @param userdata the `struct TALER_EXCHANGE_GetKeysHandle` + * @return `size * nitems` on success (everything else aborts) + */ +static size_t +header_cb (char *buffer, + size_t size, + size_t nitems, + void *userdata) +{ + struct TALER_EXCHANGE_GetKeysHandle *kr = userdata; + size_t total = size * nitems; + char *val; + + if (total < strlen (MHD_HTTP_HEADER_EXPIRES ": ")) + return total; + if (0 != strncasecmp (MHD_HTTP_HEADER_EXPIRES ": ", + buffer, + strlen (MHD_HTTP_HEADER_EXPIRES ": "))) + return total; + val = GNUNET_strndup (&buffer[strlen (MHD_HTTP_HEADER_EXPIRES ": ")], + total - strlen (MHD_HTTP_HEADER_EXPIRES ": ")); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found %s header `%s'\n", + MHD_HTTP_HEADER_EXPIRES, + val); + if (GNUNET_OK != + parse_date_string (val, + &kr->expire)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s-header `%s'\n", + MHD_HTTP_HEADER_EXPIRES, + val); + kr->expire = GNUNET_TIME_UNIT_ZERO_TS; + } + GNUNET_free (val); + return total; +} + + +/** + * Callback used when downloading the reply to a /keys request + * is complete. + * + * @param cls the `struct TALER_EXCHANGE_GetKeysHandle` + * @param response_code HTTP response code, 0 on error + * @param resp_obj parsed JSON result, NULL on error + */ +static void +keys_completed_cb (void *cls, + long response_code, + const void *resp_obj) +{ + struct TALER_EXCHANGE_GetKeysHandle *gkh = cls; + const json_t *j = resp_obj; + struct TALER_EXCHANGE_Keys *kd = NULL; + struct TALER_EXCHANGE_KeysResponse kresp = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code, + .details.ok.compat = TALER_EXCHANGE_VC_PROTOCOL_ERROR, + }; + + gkh->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received keys from URL `%s' with status %ld and expiration %s.\n", + gkh->url, + response_code, + GNUNET_TIME_timestamp2s (gkh->expire)); + if (GNUNET_TIME_absolute_is_past (gkh->expire.abs_time)) + { + if (MHD_HTTP_OK == response_code) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Exchange failed to give expiration time, assuming in %s\n", + GNUNET_TIME_relative2s (DEFAULT_EXPIRATION, + true)); + gkh->expire + = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_relative_to_absolute (DEFAULT_EXPIRATION)); + } + switch (response_code) + { + case 0: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to receive /keys response from exchange %s\n", + gkh->exchange_url); + break; + case MHD_HTTP_OK: + if (NULL == j) + { + GNUNET_break (0); + response_code = 0; + break; + } + kd = GNUNET_new (struct TALER_EXCHANGE_Keys); + kd->exchange_url = GNUNET_strdup (gkh->exchange_url); + if (NULL != gkh->prev_keys) + { + const struct TALER_EXCHANGE_Keys *kd_old = gkh->prev_keys; + + /* We keep the denomination keys and auditor signatures from the + previous iteration (/keys cherry picking) */ + kd->num_denom_keys + = kd_old->num_denom_keys; + kd->last_denom_issue_date + = kd_old->last_denom_issue_date; + GNUNET_array_grow (kd->denom_keys, + kd->denom_keys_size, + kd->num_denom_keys); + /* First make a shallow copy, we then need another pass for the RSA key... */ + GNUNET_memcpy (kd->denom_keys, + kd_old->denom_keys, + kd_old->num_denom_keys + * sizeof (struct TALER_EXCHANGE_DenomPublicKey)); + for (unsigned int i = 0; i<kd_old->num_denom_keys; i++) + TALER_denom_pub_copy (&kd->denom_keys[i].key, + &kd_old->denom_keys[i].key); + kd->num_auditors = kd_old->num_auditors; + kd->auditors + = GNUNET_new_array (kd->num_auditors, + struct TALER_EXCHANGE_AuditorInformation); + /* Now the necessary deep copy... */ + for (unsigned int i = 0; i<kd_old->num_auditors; i++) + { + const struct TALER_EXCHANGE_AuditorInformation *aold = + &kd_old->auditors[i]; + struct TALER_EXCHANGE_AuditorInformation *anew = &kd->auditors[i]; + + anew->auditor_pub = aold->auditor_pub; + anew->auditor_url = GNUNET_strdup (aold->auditor_url); + anew->auditor_name = GNUNET_strdup (aold->auditor_name); + GNUNET_array_grow (anew->denom_keys, + anew->num_denom_keys, + aold->num_denom_keys); + GNUNET_memcpy ( + anew->denom_keys, + aold->denom_keys, + aold->num_denom_keys + * sizeof (struct TALER_EXCHANGE_AuditorDenominationInfo)); + } + } + /* Now decode fresh /keys response */ + if (GNUNET_OK != + TALER_EXCHANGE_decode_keys_json_ (j, + true, + kd, + &kresp.details.ok.compat)) + { + TALER_LOG_ERROR ("Could not decode /keys response\n"); + kd->rc = 1; + TALER_EXCHANGE_keys_decref (kd); + kd = NULL; + kresp.hr.http_status = 0; + kresp.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + kd->rc = 1; + kd->key_data_expiration = gkh->expire; + if (GNUNET_TIME_relative_cmp ( + GNUNET_TIME_absolute_get_remaining (gkh->expire.abs_time), + <, + MINIMUM_EXPIRATION)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Exchange returned keys with expiration time below %s. Compensating.\n", + GNUNET_TIME_relative2s (MINIMUM_EXPIRATION, + true)); + kd->key_data_expiration + = GNUNET_TIME_relative_to_timestamp (MINIMUM_EXPIRATION); + } + + kresp.details.ok.keys = kd; + break; + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_UNAUTHORIZED: + case MHD_HTTP_FORBIDDEN: + case MHD_HTTP_NOT_FOUND: + if (NULL == j) + { + kresp.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + kresp.hr.hint = TALER_ErrorCode_get_hint (kresp.hr.ec); + } + else + { + kresp.hr.ec = TALER_JSON_get_error_code (j); + kresp.hr.hint = TALER_JSON_get_error_hint (j); + } + break; + default: + if (NULL == j) + { + kresp.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + kresp.hr.hint = TALER_ErrorCode_get_hint (kresp.hr.ec); + } + else + { + kresp.hr.ec = TALER_JSON_get_error_code (j); + kresp.hr.hint = TALER_JSON_get_error_hint (j); + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d\n", + (unsigned int) response_code, + (int) kresp.hr.ec); + break; + } + gkh->cert_cb (gkh->cert_cb_cls, + &kresp, + kd); + TALER_EXCHANGE_get_keys_cancel (gkh); +} + + +struct TALER_EXCHANGE_GetKeysHandle * +TALER_EXCHANGE_get_keys_create ( + struct GNUNET_CURL_Context *ctx, + const char *url) +{ + struct TALER_EXCHANGE_GetKeysHandle *gkh; + + gkh = GNUNET_new (struct TALER_EXCHANGE_GetKeysHandle); + gkh->ctx = ctx; + gkh->exchange_url = GNUNET_strdup (url); + return gkh; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_get_keys_set_options_ ( + struct TALER_EXCHANGE_GetKeysHandle *gkh, + unsigned int num_options, + const struct TALER_EXCHANGE_GetKeysOptionValue *options) +{ + for (unsigned int i = 0; i < num_options; i++) + { + const struct TALER_EXCHANGE_GetKeysOptionValue *opt = &options[i]; + + switch (opt->option) + { + case TALER_EXCHANGE_GET_KEYS_OPTION_END: + return GNUNET_OK; + case TALER_EXCHANGE_GET_KEYS_OPTION_LAST_KEYS: + TALER_EXCHANGE_keys_decref (gkh->prev_keys); + gkh->prev_keys = NULL; + if (NULL != opt->details.last_keys) + gkh->prev_keys + = TALER_EXCHANGE_keys_incref (opt->details.last_keys); + break; + } + } + return GNUNET_OK; +} + + +enum TALER_ErrorCode +TALER_EXCHANGE_get_keys_start ( + struct TALER_EXCHANGE_GetKeysHandle *gkh, + TALER_EXCHANGE_GetKeysCallback cert_cb, + TALER_EXCHANGE_GET_KEYS_RESULT_CLOSURE *cert_cb_cls) +{ + CURL *eh; + char last_date[80] = { 0 }; + + gkh->cert_cb = cert_cb; + gkh->cert_cb_cls = cert_cb_cls; + if (NULL != gkh->prev_keys) + { + TALER_LOG_DEBUG ("Last DK issue date (before GETting /keys): %s\n", + GNUNET_TIME_timestamp2s ( + gkh->prev_keys->last_denom_issue_date)); + GNUNET_snprintf (last_date, + sizeof (last_date), + "%llu", + (unsigned long long) + gkh->prev_keys->last_denom_issue_date.abs_time.abs_value_us + / 1000000LLU); + } + gkh->url = TALER_url_join (gkh->exchange_url, + "keys", + (NULL != gkh->prev_keys) + ? "last_issue_date" + : NULL, + (NULL != gkh->prev_keys) + ? last_date + : NULL, + NULL); + if (NULL == gkh->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + return TALER_EC_GENERIC_CONFIGURATION_INVALID; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Requesting keys with URL `%s'.\n", + gkh->url); + eh = TALER_EXCHANGE_curl_easy_get_ (gkh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (gkh->url); + gkh->url = NULL; + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_VERBOSE, + 0)); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT, + 120 /* seconds */)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &header_cb)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + gkh)); + gkh->job = GNUNET_CURL_job_add_with_ct_json (gkh->ctx, + eh, + &keys_completed_cb, + gkh); + if (NULL == gkh->job) + { + GNUNET_free (gkh->url); + gkh->url = NULL; + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + return TALER_EC_NONE; +} + + +void +TALER_EXCHANGE_get_keys_cancel ( + struct TALER_EXCHANGE_GetKeysHandle *gkh) +{ + if (NULL != gkh->job) + { + GNUNET_CURL_job_cancel (gkh->job); + gkh->job = NULL; + } + TALER_EXCHANGE_keys_decref (gkh->prev_keys); + GNUNET_free (gkh->exchange_url); + GNUNET_free (gkh->url); + GNUNET_free (gkh); +} + + +/* end of exchange_api_get-keys.c */ diff --git a/src/lib/exchange_api_post-withdraw.c b/src/lib/exchange_api_post-withdraw.c @@ -120,136 +120,6 @@ struct BlindingPrepareCoinData /** - * A /withdraw request-handle for calls with pre-blinded planchets. - * Returned by TALER_EXCHANGE_post_withdraw_blinded_create. - */ -struct TALER_EXCHANGE_PostWithdrawBlindedHandle -{ - - /** - * Reserve private key. - */ - const struct TALER_ReservePrivateKeyP *reserve_priv; - - /** - * Reserve public key, calculated - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Signature of the reserve for the request, calculated after all - * parameters for the coins are collected. - */ - struct TALER_ReserveSignatureP reserve_sig; - - /* - * The denomination keys of the exchange - */ - struct TALER_EXCHANGE_Keys *keys; - - /** - * The hash of all the planchets - */ - struct TALER_HashBlindedPlanchetsP planchets_h; - - /** - * Seed used for the derival of blinding factors for denominations - * with Clause-Schnorr cipher. - */ - const struct TALER_BlindingMasterSeedP *blinding_seed; - - /** - * Total amount requested (without fee). - */ - struct TALER_Amount amount; - - /** - * Total withdraw fee - */ - struct TALER_Amount fee; - - /** - * Is this call for age-restricted coins, with age proof? - */ - bool with_age_proof; - - /** - * If @e with_age_proof is true or @max_age is > 0, - * the age mask to use, extracted from the denominations. - * MUST be the same for all denominations. - */ - struct TALER_AgeMask age_mask; - - /** - * The maximum age to commit to. If @e with_age_proof - * is true, the client will need to proof the correct setting - * of age-restriction on the coins via an additional call - * to /reveal-withdraw. - */ - uint8_t max_age; - - /** - * If @e with_age_proof is true, the hash of all the selected planchets - */ - struct TALER_HashBlindedPlanchetsP selected_h; - - /** - * Length of the either the @e blinded.input or - * the @e blinded.with_age_proof_input array, - * depending on @e with_age_proof. - */ - size_t num_input; - - union - { - /** - * The blinded planchet input candidates for age-restricted coins - * for the call to /withdraw - */ - const struct - TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput *with_age_proof_input; - - /** - * The blinded planchet input for the call to /withdraw, - * for age-unrestricted coins. - */ - const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *input; - - } blinded; - - /** - * The url for this request. - */ - char *request_url; - - /** - * Context for curl. - */ - struct GNUNET_CURL_Context *curl_ctx; - - /** - * CURL handle for the request job. - */ - struct GNUNET_CURL_Job *job; - - /** - * Post Context - */ - struct TALER_CURL_PostContext post_ctx; - - /** - * Function to call with withdraw response results. - */ - TALER_EXCHANGE_PostWithdrawBlindedCallback callback; - - /** - * Closure for @e callback - */ - void *callback_cls; -}; - - -/** * A /withdraw request-handle for calls from * a wallet, i. e. when blinding data is available. */ @@ -427,564 +297,6 @@ struct TALER_EXCHANGE_PostWithdrawHandle }; -/* Forward declarations */ -static enum TALER_ErrorCode -call_withdraw_blinded ( - struct TALER_EXCHANGE_PostWithdrawHandle *wh); - - -/** - * We got a 200 OK response for the /withdraw operation. - * Extract the signatures and return them to the caller. - * - * @param wbh operation handle - * @param j_response reply from the exchange - * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors - */ -static enum GNUNET_GenericReturnValue -withdraw_blinded_ok ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh, - const json_t *j_response) -{ - struct TALER_EXCHANGE_PostWithdrawBlindedResponse response = { - .hr.reply = j_response, - .hr.http_status = MHD_HTTP_OK, - }; - const json_t *j_sigs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("ev_sigs", - &j_sigs), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j_response, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (wbh->num_input != json_array_size (j_sigs)) - { - /* Number of coins generated does not match our expectation */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - { - struct TALER_BlindedDenominationSignature denoms_sig[wbh->num_input]; - - memset (denoms_sig, - 0, - sizeof(denoms_sig)); - - /* Reconstruct the coins and unblind the signatures */ - { - json_t *j_sig; - size_t i; - - json_array_foreach (j_sigs, i, j_sig) - { - struct GNUNET_JSON_Specification ispec[] = { - TALER_JSON_spec_blinded_denom_sig (NULL, - &denoms_sig[i]), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j_sig, - ispec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - } - - response.details.ok.num_sigs = wbh->num_input; - response.details.ok.blinded_denom_sigs = denoms_sig; - response.details.ok.planchets_h = wbh->planchets_h; - wbh->callback ( - wbh->callback_cls, - &response); - /* Make sure the callback isn't called again */ - wbh->callback = NULL; - /* Free resources */ - for (size_t i = 0; i < wbh->num_input; i++) - TALER_blinded_denom_sig_free (&denoms_sig[i]); - } - - return GNUNET_OK; -} - - -/** - * We got a 201 CREATED response for the /withdraw operation. - * Extract the noreveal_index and return it to the caller. - * - * @param wbh operation handle - * @param j_response reply from the exchange - * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors - */ -static enum GNUNET_GenericReturnValue -withdraw_blinded_created ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh, - const json_t *j_response) -{ - struct TALER_EXCHANGE_PostWithdrawBlindedResponse response = { - .hr.reply = j_response, - .hr.http_status = MHD_HTTP_CREATED, - .details.created.planchets_h = wbh->planchets_h, - .details.created.num_coins = wbh->num_input, - }; - struct TALER_ExchangeSignatureP exchange_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_uint8 ("noreveal_index", - &response.details.created.noreveal_index), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &response.details.created.exchange_pub), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK!= - GNUNET_JSON_parse (j_response, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (GNUNET_OK != - TALER_exchange_online_withdraw_age_confirmation_verify ( - &wbh->planchets_h, - response.details.created.noreveal_index, - &response.details.created.exchange_pub, - &exchange_sig)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - - } - - wbh->callback (wbh->callback_cls, - &response); - /* make sure the callback isn't called again */ - wbh->callback = NULL; - - return GNUNET_OK; -} - - -/** - * Function called when we're done processing the - * HTTP /withdraw request. - * - * @param cls the `struct TALER_EXCHANGE_PostWithdrawBlindedHandle` - * @param response_code The HTTP response code - * @param response response data - */ -static void -handle_withdraw_blinded_finished ( - void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh = cls; - const json_t *j_response = response; - struct TALER_EXCHANGE_PostWithdrawBlindedResponse wbr = { - .hr.reply = j_response, - .hr.http_status = (unsigned int) response_code - }; - - wbh->job = NULL; - switch (response_code) - { - case 0: - wbr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - { - if (GNUNET_OK != - withdraw_blinded_ok ( - wbh, - j_response)) - { - GNUNET_break_op (0); - wbr.hr.http_status = 0; - wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - GNUNET_assert (NULL == wbh->callback); - TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh); - return; - } - case MHD_HTTP_CREATED: - if (GNUNET_OK != - withdraw_blinded_created ( - wbh, - j_response)) - { - GNUNET_break_op (0); - wbr.hr.http_status = 0; - wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - GNUNET_assert (NULL == wbh->callback); - TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh); - return; - 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 */ - wbr.hr.ec = TALER_JSON_get_error_code (j_response); - wbr.hr.hint = TALER_JSON_get_error_hint (j_response); - 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 */ - wbr.hr.ec = TALER_JSON_get_error_code (j_response); - wbr.hr.hint = TALER_JSON_get_error_hint (j_response); - 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. */ - wbr.hr.ec = TALER_JSON_get_error_code (j_response); - wbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_CONFLICT: - /* The age requirements might not have been met */ - wbr.hr.ec = TALER_JSON_get_error_code (j_response); - wbr.hr.hint = TALER_JSON_get_error_hint (j_response); - 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 */ - wbr.hr.ec = TALER_JSON_get_error_code (j_response); - wbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: - /* only validate reply is well-formed */ - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ( - "h_payto", - &wbr.details.unavailable_for_legal_reasons.h_payto), - GNUNET_JSON_spec_uint64 ( - "requirement_row", - &wbr.details.unavailable_for_legal_reasons.requirement_row), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j_response, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - wbr.hr.http_status = 0; - wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - break; - } - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - wbr.hr.ec = TALER_JSON_get_error_code (j_response); - wbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - default: - /* unexpected response code */ - GNUNET_break_op (0); - wbr.hr.ec = TALER_JSON_get_error_code (j_response); - wbr.hr.hint = TALER_JSON_get_error_hint (j_response); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for exchange withdraw\n", - (unsigned int) response_code, - (int) wbr.hr.ec); - break; - } - wbh->callback (wbh->callback_cls, - &wbr); - TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh); -} - - -/** - * Runs the actual withdraw operation with the blinded planchets. - * - * @param[in,out] wbh withdraw blinded handle - * @return #TALER_EC_NONE on success, error code on failure - */ -static enum TALER_ErrorCode -perform_withdraw_protocol ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh) -{ -#define FAIL_IF(cond) \ - do { \ - if ((cond)) \ - { \ - GNUNET_break (! (cond)); \ - goto ERROR; \ - } \ - } while (0) - - json_t * j_denoms = NULL; - json_t *j_planchets = NULL; - json_t *j_request_body = NULL; - CURL *curlh = NULL; - struct GNUNET_HashContext *coins_hctx = NULL; - struct TALER_BlindedCoinHashP bch; - - GNUNET_assert (0 < wbh->num_input); - - FAIL_IF (GNUNET_OK != - TALER_amount_set_zero (wbh->keys->currency, - &wbh->amount)); - FAIL_IF (GNUNET_OK != - TALER_amount_set_zero (wbh->keys->currency, - &wbh->fee)); - - /* Accumulate total value with fees */ - for (size_t i = 0; i < wbh->num_input; i++) - { - const struct TALER_EXCHANGE_DenomPublicKey *dpub = - wbh->with_age_proof ? - wbh->blinded.with_age_proof_input[i].denom_pub : - wbh->blinded.input[i].denom_pub; - - FAIL_IF (0 > - TALER_amount_add (&wbh->amount, - &wbh->amount, - &dpub->value)); - FAIL_IF (0 > - TALER_amount_add (&wbh->fee, - &wbh->fee, - &dpub->fees.withdraw)); - - if (GNUNET_CRYPTO_BSA_CS == - dpub->key.bsign_pub_key->cipher) - GNUNET_assert (NULL != wbh->blinding_seed); - - } - - if (wbh->with_age_proof || wbh->max_age > 0) - { - wbh->age_mask = - wbh->blinded.with_age_proof_input[0].denom_pub->key.age_mask; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Attempting to withdraw from reserve %s with maximum age %d to proof\n", - TALER_B2S (&wbh->reserve_pub), - wbh->max_age); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Attempting to withdraw from reserve %s\n", - TALER_B2S (&wbh->reserve_pub)); - } - - coins_hctx = GNUNET_CRYPTO_hash_context_start (); - FAIL_IF (NULL == coins_hctx); - - j_denoms = json_array (); - j_planchets = json_array (); - FAIL_IF ((NULL == j_denoms) || - (NULL == j_planchets)); - - for (size_t i = 0; i< wbh->num_input; i++) - { - /* Build the denomination array */ - const struct TALER_EXCHANGE_DenomPublicKey *denom_pub = - wbh->with_age_proof ? - wbh->blinded.with_age_proof_input[i].denom_pub : - wbh->blinded.input[i].denom_pub; - const struct TALER_DenominationHashP *denom_h = &denom_pub->h_key; - json_t *jdenom; - - /* The mask must be the same for all coins */ - FAIL_IF (wbh->with_age_proof && - (wbh->age_mask.bits != denom_pub->key.age_mask.bits)); - - jdenom = GNUNET_JSON_from_data_auto (denom_h); - FAIL_IF (NULL == jdenom); - FAIL_IF (0 > json_array_append_new (j_denoms, - jdenom)); - } - - - /* Build the planchet array and calculate the hash over all planchets. */ - if (! wbh->with_age_proof) - { - for (size_t i = 0; i< wbh->num_input; i++) - { - const struct TALER_PlanchetDetail *planchet = - &wbh->blinded.input[i].planchet_details; - json_t *jc = GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_planchet ( - NULL, - &planchet->blinded_planchet)); - FAIL_IF (NULL == jc); - FAIL_IF (0 > json_array_append_new (j_planchets, - jc)); - - TALER_coin_ev_hash (&planchet->blinded_planchet, - &planchet->denom_pub_hash, - &bch); - - GNUNET_CRYPTO_hash_context_read (coins_hctx, - &bch, - sizeof(bch)); - } - } - else - { /* Age restricted case with required age-proof. */ - - /** - * We collect the run of all coin candidates for the same γ index - * first, then γ+1 etc. - */ - for (size_t k = 0; k < TALER_CNC_KAPPA; k++) - { - struct GNUNET_HashContext *batch_ctx; - struct TALER_BlindedCoinHashP batch_h; - - batch_ctx = GNUNET_CRYPTO_hash_context_start (); - FAIL_IF (NULL == batch_ctx); - - for (size_t i = 0; i< wbh->num_input; i++) - { - const struct TALER_PlanchetDetail *planchet = - &wbh->blinded.with_age_proof_input[i].planchet_details[k]; - json_t *jc = GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_planchet ( - NULL, - &planchet->blinded_planchet)); - - FAIL_IF (NULL == jc); - FAIL_IF (0 > json_array_append_new ( - j_planchets, - jc)); - - TALER_coin_ev_hash ( - &planchet->blinded_planchet, - &planchet->denom_pub_hash, - &bch); - - GNUNET_CRYPTO_hash_context_read ( - batch_ctx, - &bch, - sizeof(bch)); - } - - GNUNET_CRYPTO_hash_context_finish ( - batch_ctx, - &batch_h.hash); - GNUNET_CRYPTO_hash_context_read ( - coins_hctx, - &batch_h, - sizeof(batch_h)); - } - } - - /* Build the hash of the planchets */ - GNUNET_CRYPTO_hash_context_finish ( - coins_hctx, - &wbh->planchets_h.hash); - coins_hctx = NULL; - - /* Sign the request */ - TALER_wallet_withdraw_sign ( - &wbh->amount, - &wbh->fee, - &wbh->planchets_h, - wbh->blinding_seed, - wbh->with_age_proof ? &wbh->age_mask: NULL, - wbh->with_age_proof ? wbh->max_age : 0, - wbh->reserve_priv, - &wbh->reserve_sig); - - /* Initiate the POST-request */ - j_request_body = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("cipher", - "ED25519"), - GNUNET_JSON_pack_data_auto ("reserve_pub", - &wbh->reserve_pub), - GNUNET_JSON_pack_array_steal ("denoms_h", - j_denoms), - GNUNET_JSON_pack_array_steal ("coin_evs", - j_planchets), - GNUNET_JSON_pack_allow_null ( - wbh->with_age_proof - ? GNUNET_JSON_pack_int64 ("max_age", - wbh->max_age) - : GNUNET_JSON_pack_string ("max_age", - NULL) ), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &wbh->reserve_sig)); - FAIL_IF (NULL == j_request_body); - - if (NULL != wbh->blinding_seed) - { - json_t *j_seed = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("blinding_seed", - wbh->blinding_seed)); - GNUNET_assert (NULL != j_seed); - GNUNET_assert (0 == - json_object_update_new ( - j_request_body, - j_seed)); - } - - curlh = TALER_EXCHANGE_curl_easy_get_ (wbh->request_url); - FAIL_IF (NULL == curlh); - FAIL_IF (GNUNET_OK != - TALER_curl_easy_post ( - &wbh->post_ctx, - curlh, - j_request_body)); - json_decref (j_request_body); - j_request_body = NULL; - - wbh->job = GNUNET_CURL_job_add2 ( - wbh->curl_ctx, - curlh, - wbh->post_ctx.headers, - &handle_withdraw_blinded_finished, - wbh); - FAIL_IF (NULL == wbh->job); - - /* No errors, return */ - return TALER_EC_NONE; - -ERROR: - if (NULL != coins_hctx) - GNUNET_CRYPTO_hash_context_abort (coins_hctx); - if (NULL != j_denoms) - json_decref (j_denoms); - if (NULL != j_planchets) - json_decref (j_planchets); - if (NULL != j_request_body) - json_decref (j_request_body); - if (NULL != curlh) - curl_easy_cleanup (curlh); - return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; -#undef FAIL_IF -} - - /** * @brief Callback to copy the results from the call to post_withdraw_blinded * in the non-age-restricted case to the result for the originating call. @@ -1842,121 +1154,4 @@ TALER_EXCHANGE_post_withdraw_cancel ( } -/** - * @brief Prepare the handler for blinded withdraw - * - * Allocates the handler struct and prepares all fields of the handler - * except the blinded planchets, - * which depend on them being age-restricted or not. - * - * @param curl_ctx the context for curl - * @param keys the exchange keys - * @param exchange_url the url to the exchange - * @param reserve_priv the reserve's private key - * @return the handler, NULL on error - */ -static struct TALER_EXCHANGE_PostWithdrawBlindedHandle * -setup_handler_common ( - struct GNUNET_CURL_Context *curl_ctx, - struct TALER_EXCHANGE_Keys *keys, - const char *exchange_url, - const struct TALER_ReservePrivateKeyP *reserve_priv) -{ - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh = - GNUNET_new (struct TALER_EXCHANGE_PostWithdrawBlindedHandle); - - wbh->keys = TALER_EXCHANGE_keys_incref (keys); - wbh->curl_ctx = curl_ctx; - wbh->reserve_priv = reserve_priv; - wbh->request_url = TALER_url_join (exchange_url, - "withdraw", - NULL); - GNUNET_CRYPTO_eddsa_key_get_public ( - &wbh->reserve_priv->eddsa_priv, - &wbh->reserve_pub.eddsa_pub); - - return wbh; -} - - -struct TALER_EXCHANGE_PostWithdrawBlindedHandle * -TALER_EXCHANGE_post_withdraw_blinded_create ( - struct GNUNET_CURL_Context *curl_ctx, - struct TALER_EXCHANGE_Keys *keys, - const char *exchange_url, - const struct TALER_ReservePrivateKeyP *reserve_priv, - const struct TALER_BlindingMasterSeedP *blinding_seed, - size_t num_input, - const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *blinded_input) -{ - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh = - setup_handler_common (curl_ctx, - keys, - exchange_url, - reserve_priv); - - wbh->with_age_proof = false; - wbh->num_input = num_input; - wbh->blinded.input = blinded_input; - wbh->blinding_seed = blinding_seed; - - return wbh; -} - - -enum GNUNET_GenericReturnValue -TALER_EXCHANGE_post_withdraw_blinded_set_options_ ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh, - unsigned int num_options, - const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue options[]) -{ - for (unsigned int i = 0; i < num_options; i++) - { - const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue *opt = - &options[i]; - switch (opt->option) - { - case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_END: - return GNUNET_OK; - case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF: - pwbh->with_age_proof = true; - pwbh->max_age = opt->details.with_age_proof.max_age; - pwbh->blinded.with_age_proof_input = opt->details.with_age_proof.input; - break; - } - } - return GNUNET_OK; -} - - -enum TALER_ErrorCode -TALER_EXCHANGE_post_withdraw_blinded_start ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh, - TALER_EXCHANGE_PostWithdrawBlindedCallback cb, - TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE *cb_cls) -{ - pwbh->callback = cb; - pwbh->callback_cls = cb_cls; - return perform_withdraw_protocol (pwbh); -} - - -void -TALER_EXCHANGE_post_withdraw_blinded_cancel ( - struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh) -{ - if (NULL == wbh) - return; - if (NULL != wbh->job) - { - GNUNET_CURL_job_cancel (wbh->job); - wbh->job = NULL; - } - GNUNET_free (wbh->request_url); - TALER_EXCHANGE_keys_decref (wbh->keys); - TALER_curl_easy_post_finished (&wbh->post_ctx); - GNUNET_free (wbh); -} - - /* exchange_api_post-withdraw.c */ diff --git a/src/lib/exchange_api_post-withdraw_blinded.c b/src/lib/exchange_api_post-withdraw_blinded.c @@ -0,0 +1,840 @@ +/* + This file is part of TALER + Copyright (C) 2023-2026 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_post-withdraw.c + * @brief Implementation of /withdraw requests + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include <gnunet/gnunet_common.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <sys/wait.h> +#include "taler/taler_curl_lib.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_exchange_service.h" +#include "exchange_api_common.h" +#include "exchange_api_handle.h" +#include "taler/taler_signatures.h" +#include "exchange_api_curl_defaults.h" +#include "taler/taler_util.h" + + +/** + * A /withdraw request-handle for calls with pre-blinded planchets. + * Returned by TALER_EXCHANGE_post_withdraw_blinded_create. + */ +struct TALER_EXCHANGE_PostWithdrawBlindedHandle +{ + + /** + * Reserve private key. + */ + const struct TALER_ReservePrivateKeyP *reserve_priv; + + /** + * Reserve public key, calculated + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Signature of the reserve for the request, calculated after all + * parameters for the coins are collected. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /* + * The denomination keys of the exchange + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The hash of all the planchets + */ + struct TALER_HashBlindedPlanchetsP planchets_h; + + /** + * Seed used for the derival of blinding factors for denominations + * with Clause-Schnorr cipher. + */ + const struct TALER_BlindingMasterSeedP *blinding_seed; + + /** + * Total amount requested (without fee). + */ + struct TALER_Amount amount; + + /** + * Total withdraw fee + */ + struct TALER_Amount fee; + + /** + * Is this call for age-restricted coins, with age proof? + */ + bool with_age_proof; + + /** + * If @e with_age_proof is true or @max_age is > 0, + * the age mask to use, extracted from the denominations. + * MUST be the same for all denominations. + */ + struct TALER_AgeMask age_mask; + + /** + * The maximum age to commit to. If @e with_age_proof + * is true, the client will need to proof the correct setting + * of age-restriction on the coins via an additional call + * to /reveal-withdraw. + */ + uint8_t max_age; + + /** + * If @e with_age_proof is true, the hash of all the selected planchets + */ + struct TALER_HashBlindedPlanchetsP selected_h; + + /** + * Length of the either the @e blinded.input or + * the @e blinded.with_age_proof_input array, + * depending on @e with_age_proof. + */ + size_t num_input; + + union + { + /** + * The blinded planchet input candidates for age-restricted coins + * for the call to /withdraw + */ + const struct + TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput *with_age_proof_input; + + /** + * The blinded planchet input for the call to /withdraw, + * for age-unrestricted coins. + */ + const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *input; + + } blinded; + + /** + * The url for this request. + */ + char *request_url; + + /** + * Context for curl. + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * CURL handle for the request job. + */ + struct GNUNET_CURL_Job *job; + + /** + * Post Context + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with withdraw response results. + */ + TALER_EXCHANGE_PostWithdrawBlindedCallback callback; + + /** + * Closure for @e callback + */ + void *callback_cls; +}; + + +/** + * We got a 200 OK response for the /withdraw operation. + * Extract the signatures and return them to the caller. + * + * @param wbh operation handle + * @param j_response reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +withdraw_blinded_ok ( + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh, + const json_t *j_response) +{ + struct TALER_EXCHANGE_PostWithdrawBlindedResponse response = { + .hr.reply = j_response, + .hr.http_status = MHD_HTTP_OK, + }; + const json_t *j_sigs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("ev_sigs", + &j_sigs), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (wbh->num_input != json_array_size (j_sigs)) + { + /* Number of coins generated does not match our expectation */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + { + struct TALER_BlindedDenominationSignature denoms_sig[wbh->num_input]; + + memset (denoms_sig, + 0, + sizeof(denoms_sig)); + + /* Reconstruct the coins and unblind the signatures */ + { + json_t *j_sig; + size_t i; + + json_array_foreach (j_sigs, i, j_sig) + { + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_blinded_denom_sig (NULL, + &denoms_sig[i]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_sig, + ispec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + } + + response.details.ok.num_sigs = wbh->num_input; + response.details.ok.blinded_denom_sigs = denoms_sig; + response.details.ok.planchets_h = wbh->planchets_h; + wbh->callback ( + wbh->callback_cls, + &response); + /* Make sure the callback isn't called again */ + wbh->callback = NULL; + /* Free resources */ + for (size_t i = 0; i < wbh->num_input; i++) + TALER_blinded_denom_sig_free (&denoms_sig[i]); + } + + return GNUNET_OK; +} + + +/** + * We got a 201 CREATED response for the /withdraw operation. + * Extract the noreveal_index and return it to the caller. + * + * @param wbh operation handle + * @param j_response reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +withdraw_blinded_created ( + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh, + const json_t *j_response) +{ + struct TALER_EXCHANGE_PostWithdrawBlindedResponse response = { + .hr.reply = j_response, + .hr.http_status = MHD_HTTP_CREATED, + .details.created.planchets_h = wbh->planchets_h, + .details.created.num_coins = wbh->num_input, + }; + struct TALER_ExchangeSignatureP exchange_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint8 ("noreveal_index", + &response.details.created.noreveal_index), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &response.details.created.exchange_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK!= + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_exchange_online_withdraw_age_confirmation_verify ( + &wbh->planchets_h, + response.details.created.noreveal_index, + &response.details.created.exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + + } + + wbh->callback (wbh->callback_cls, + &response); + /* make sure the callback isn't called again */ + wbh->callback = NULL; + + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /withdraw request. + * + * @param cls the `struct TALER_EXCHANGE_PostWithdrawBlindedHandle` + * @param response_code The HTTP response code + * @param response response data + */ +static void +handle_withdraw_blinded_finished ( + void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh = cls; + const json_t *j_response = response; + struct TALER_EXCHANGE_PostWithdrawBlindedResponse wbr = { + .hr.reply = j_response, + .hr.http_status = (unsigned int) response_code + }; + + wbh->job = NULL; + switch (response_code) + { + case 0: + wbr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + if (GNUNET_OK != + withdraw_blinded_ok ( + wbh, + j_response)) + { + GNUNET_break_op (0); + wbr.hr.http_status = 0; + wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == wbh->callback); + TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh); + return; + } + case MHD_HTTP_CREATED: + if (GNUNET_OK != + withdraw_blinded_created ( + wbh, + j_response)) + { + GNUNET_break_op (0); + wbr.hr.http_status = 0; + wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == wbh->callback); + TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh); + return; + 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 */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + 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 */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + 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. */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_CONFLICT: + /* The age requirements might not have been met */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + 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 */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + /* only validate reply is well-formed */ + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ( + "h_payto", + &wbr.details.unavailable_for_legal_reasons.h_payto), + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &wbr.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + wbr.hr.http_status = 0; + wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + } + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange withdraw\n", + (unsigned int) response_code, + (int) wbr.hr.ec); + break; + } + wbh->callback (wbh->callback_cls, + &wbr); + TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh); +} + + +/** + * Runs the actual withdraw operation with the blinded planchets. + * + * @param[in,out] wbh withdraw blinded handle + * @return #TALER_EC_NONE on success, error code on failure + */ +static enum TALER_ErrorCode +perform_withdraw_protocol ( + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh) +{ +#define FAIL_IF(cond) \ + do { \ + if ((cond)) \ + { \ + GNUNET_break (! (cond)); \ + goto ERROR; \ + } \ + } while (0) + + json_t * j_denoms = NULL; + json_t *j_planchets = NULL; + json_t *j_request_body = NULL; + CURL *curlh = NULL; + struct GNUNET_HashContext *coins_hctx = NULL; + struct TALER_BlindedCoinHashP bch; + + GNUNET_assert (0 < wbh->num_input); + + FAIL_IF (GNUNET_OK != + TALER_amount_set_zero (wbh->keys->currency, + &wbh->amount)); + FAIL_IF (GNUNET_OK != + TALER_amount_set_zero (wbh->keys->currency, + &wbh->fee)); + + /* Accumulate total value with fees */ + for (size_t i = 0; i < wbh->num_input; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dpub = + wbh->with_age_proof ? + wbh->blinded.with_age_proof_input[i].denom_pub : + wbh->blinded.input[i].denom_pub; + + FAIL_IF (0 > + TALER_amount_add (&wbh->amount, + &wbh->amount, + &dpub->value)); + FAIL_IF (0 > + TALER_amount_add (&wbh->fee, + &wbh->fee, + &dpub->fees.withdraw)); + + if (GNUNET_CRYPTO_BSA_CS == + dpub->key.bsign_pub_key->cipher) + GNUNET_assert (NULL != wbh->blinding_seed); + + } + + if (wbh->with_age_proof || wbh->max_age > 0) + { + wbh->age_mask = + wbh->blinded.with_age_proof_input[0].denom_pub->key.age_mask; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Attempting to withdraw from reserve %s with maximum age %d to proof\n", + TALER_B2S (&wbh->reserve_pub), + wbh->max_age); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Attempting to withdraw from reserve %s\n", + TALER_B2S (&wbh->reserve_pub)); + } + + coins_hctx = GNUNET_CRYPTO_hash_context_start (); + FAIL_IF (NULL == coins_hctx); + + j_denoms = json_array (); + j_planchets = json_array (); + FAIL_IF ((NULL == j_denoms) || + (NULL == j_planchets)); + + for (size_t i = 0; i< wbh->num_input; i++) + { + /* Build the denomination array */ + const struct TALER_EXCHANGE_DenomPublicKey *denom_pub = + wbh->with_age_proof ? + wbh->blinded.with_age_proof_input[i].denom_pub : + wbh->blinded.input[i].denom_pub; + const struct TALER_DenominationHashP *denom_h = &denom_pub->h_key; + json_t *jdenom; + + /* The mask must be the same for all coins */ + FAIL_IF (wbh->with_age_proof && + (wbh->age_mask.bits != denom_pub->key.age_mask.bits)); + + jdenom = GNUNET_JSON_from_data_auto (denom_h); + FAIL_IF (NULL == jdenom); + FAIL_IF (0 > json_array_append_new (j_denoms, + jdenom)); + } + + + /* Build the planchet array and calculate the hash over all planchets. */ + if (! wbh->with_age_proof) + { + for (size_t i = 0; i< wbh->num_input; i++) + { + const struct TALER_PlanchetDetail *planchet = + &wbh->blinded.input[i].planchet_details; + json_t *jc = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_planchet ( + NULL, + &planchet->blinded_planchet)); + FAIL_IF (NULL == jc); + FAIL_IF (0 > json_array_append_new (j_planchets, + jc)); + + TALER_coin_ev_hash (&planchet->blinded_planchet, + &planchet->denom_pub_hash, + &bch); + + GNUNET_CRYPTO_hash_context_read (coins_hctx, + &bch, + sizeof(bch)); + } + } + else + { /* Age restricted case with required age-proof. */ + + /** + * We collect the run of all coin candidates for the same γ index + * first, then γ+1 etc. + */ + for (size_t k = 0; k < TALER_CNC_KAPPA; k++) + { + struct GNUNET_HashContext *batch_ctx; + struct TALER_BlindedCoinHashP batch_h; + + batch_ctx = GNUNET_CRYPTO_hash_context_start (); + FAIL_IF (NULL == batch_ctx); + + for (size_t i = 0; i< wbh->num_input; i++) + { + const struct TALER_PlanchetDetail *planchet = + &wbh->blinded.with_age_proof_input[i].planchet_details[k]; + json_t *jc = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_planchet ( + NULL, + &planchet->blinded_planchet)); + + FAIL_IF (NULL == jc); + FAIL_IF (0 > json_array_append_new ( + j_planchets, + jc)); + + TALER_coin_ev_hash ( + &planchet->blinded_planchet, + &planchet->denom_pub_hash, + &bch); + + GNUNET_CRYPTO_hash_context_read ( + batch_ctx, + &bch, + sizeof(bch)); + } + + GNUNET_CRYPTO_hash_context_finish ( + batch_ctx, + &batch_h.hash); + GNUNET_CRYPTO_hash_context_read ( + coins_hctx, + &batch_h, + sizeof(batch_h)); + } + } + + /* Build the hash of the planchets */ + GNUNET_CRYPTO_hash_context_finish ( + coins_hctx, + &wbh->planchets_h.hash); + coins_hctx = NULL; + + /* Sign the request */ + TALER_wallet_withdraw_sign ( + &wbh->amount, + &wbh->fee, + &wbh->planchets_h, + wbh->blinding_seed, + wbh->with_age_proof ? &wbh->age_mask: NULL, + wbh->with_age_proof ? wbh->max_age : 0, + wbh->reserve_priv, + &wbh->reserve_sig); + + /* Initiate the POST-request */ + j_request_body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + "ED25519"), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &wbh->reserve_pub), + GNUNET_JSON_pack_array_steal ("denoms_h", + j_denoms), + GNUNET_JSON_pack_array_steal ("coin_evs", + j_planchets), + GNUNET_JSON_pack_allow_null ( + wbh->with_age_proof + ? GNUNET_JSON_pack_int64 ("max_age", + wbh->max_age) + : GNUNET_JSON_pack_string ("max_age", + NULL) ), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &wbh->reserve_sig)); + FAIL_IF (NULL == j_request_body); + + if (NULL != wbh->blinding_seed) + { + json_t *j_seed = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("blinding_seed", + wbh->blinding_seed)); + GNUNET_assert (NULL != j_seed); + GNUNET_assert (0 == + json_object_update_new ( + j_request_body, + j_seed)); + } + + curlh = TALER_EXCHANGE_curl_easy_get_ (wbh->request_url); + FAIL_IF (NULL == curlh); + FAIL_IF (GNUNET_OK != + TALER_curl_easy_post ( + &wbh->post_ctx, + curlh, + j_request_body)); + json_decref (j_request_body); + j_request_body = NULL; + + wbh->job = GNUNET_CURL_job_add2 ( + wbh->curl_ctx, + curlh, + wbh->post_ctx.headers, + &handle_withdraw_blinded_finished, + wbh); + FAIL_IF (NULL == wbh->job); + + /* No errors, return */ + return TALER_EC_NONE; + +ERROR: + if (NULL != coins_hctx) + GNUNET_CRYPTO_hash_context_abort (coins_hctx); + if (NULL != j_denoms) + json_decref (j_denoms); + if (NULL != j_planchets) + json_decref (j_planchets); + if (NULL != j_request_body) + json_decref (j_request_body); + if (NULL != curlh) + curl_easy_cleanup (curlh); + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; +#undef FAIL_IF +} + + +/** + * @brief Prepare the handler for blinded withdraw + * + * Allocates the handler struct and prepares all fields of the handler + * except the blinded planchets, + * which depend on them being age-restricted or not. + * + * @param curl_ctx the context for curl + * @param keys the exchange keys + * @param exchange_url the url to the exchange + * @param reserve_priv the reserve's private key + * @return the handler, NULL on error + */ +static struct TALER_EXCHANGE_PostWithdrawBlindedHandle * +setup_handler_common ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv) +{ + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh = + GNUNET_new (struct TALER_EXCHANGE_PostWithdrawBlindedHandle); + + wbh->keys = TALER_EXCHANGE_keys_incref (keys); + wbh->curl_ctx = curl_ctx; + wbh->reserve_priv = reserve_priv; + wbh->request_url = TALER_url_join (exchange_url, + "withdraw", + NULL); + GNUNET_CRYPTO_eddsa_key_get_public ( + &wbh->reserve_priv->eddsa_priv, + &wbh->reserve_pub.eddsa_pub); + + return wbh; +} + + +struct TALER_EXCHANGE_PostWithdrawBlindedHandle * +TALER_EXCHANGE_post_withdraw_blinded_create ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_BlindingMasterSeedP *blinding_seed, + size_t num_input, + const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *blinded_input) +{ + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh = + setup_handler_common (curl_ctx, + keys, + exchange_url, + reserve_priv); + + wbh->with_age_proof = false; + wbh->num_input = num_input; + wbh->blinded.input = blinded_input; + wbh->blinding_seed = blinding_seed; + + return wbh; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_post_withdraw_blinded_set_options_ ( + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh, + unsigned int num_options, + const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue options[]) +{ + for (unsigned int i = 0; i < num_options; i++) + { + const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue *opt = + &options[i]; + switch (opt->option) + { + case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_END: + return GNUNET_OK; + case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF: + pwbh->with_age_proof = true; + pwbh->max_age = opt->details.with_age_proof.max_age; + pwbh->blinded.with_age_proof_input = opt->details.with_age_proof.input; + break; + } + } + return GNUNET_OK; +} + + +enum TALER_ErrorCode +TALER_EXCHANGE_post_withdraw_blinded_start ( + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh, + TALER_EXCHANGE_PostWithdrawBlindedCallback cb, + TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE *cb_cls) +{ + pwbh->callback = cb; + pwbh->callback_cls = cb_cls; + return perform_withdraw_protocol (pwbh); +} + + +void +TALER_EXCHANGE_post_withdraw_blinded_cancel ( + struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh) +{ + if (NULL == wbh) + return; + if (NULL != wbh->job) + { + GNUNET_CURL_job_cancel (wbh->job); + wbh->job = NULL; + } + GNUNET_free (wbh->request_url); + TALER_EXCHANGE_keys_decref (wbh->keys); + TALER_curl_easy_post_finished (&wbh->post_ctx); + GNUNET_free (wbh); +} + + +/* exchange_api_post-withdraw_blinded.c */