diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h index ac4bc6c3c..13a238ba7 100644 --- a/src/include/taler_exchange_service.h +++ b/src/include/taler_exchange_service.h @@ -2745,7 +2745,7 @@ TALER_EXCHANGE_batch_withdraw2_cancel ( /* ********************* /reserve/$RESERVE_PUB/age-withdraw *************** */ /** - * @brief Information needed to withdraw age restricted coins. + * @brief Information needed to withdraw (and reveal) age restricted coins. */ struct TALER_EXCHANGE_AgeWithdrawCoinInput { @@ -2753,7 +2753,7 @@ struct TALER_EXCHANGE_AgeWithdrawCoinInput * The master secret from which we derive all other relevant values for * the coin: private key, nonces (if applicable) and age restriction */ - const struct TALER_PlanchetMasterSecretP secret[TALER_CNC_KAPPA]; + const struct TALER_PlanchetMasterSecretP secrets[TALER_CNC_KAPPA]; /** * The denomination of the coin. Must support age restriction, i.e @@ -2794,6 +2794,11 @@ struct TALER_EXCHANGE_AgeWithdrawResponse */ uint8_t noreveal_index; + /** + * The commitment of the call to /age-withdraw + */ + struct TALER_AgeWithdrawCommitmentHashP h_commitment; + /** * Signature of the exchange over the origina TALER_AgeWithdrawRequestPS */ @@ -2842,7 +2847,7 @@ TALER_EXCHANGE_age_withdraw ( struct TALER_EXCHANGE_Keys *keys, const struct TALER_ReservePrivateKeyP *reserve_priv, size_t num_coins, - const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[const static + const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[static num_coins], uint8_t max_age, TALER_EXCHANGE_AgeWithdrawCallback res_cb, @@ -2859,6 +2864,103 @@ TALER_EXCHANGE_age_withdraw_cancel ( struct TALER_EXCHANGE_AgeWithdrawHandle *awh); +/* ********************* /age-withdraw/$ACH/reveal ************************ */ + +/** + * @brief A handle to a /age-withdraw/$ACH/reveal request + */ +struct TALER_EXCHANGE_AgeWithdrawRevealHandle; + + +/** + * + */ + +struct TALER_EXCHANGE_AgeWithdrawRevealResponse +{ + /** + * HTTP response data. + */ + struct TALER_EXCHANGE_HttpResponse hr; + + /** + * Details about the response + */ + union + { + /** + * Details if the status is #MHD_HTTP_OK. + */ + struct + { + /** + * Number of coins returned. + */ + unsigned int num_coins; + + /** + * Array of @e num_coins values about the coins obtained via the reveal + * operation. The array give thes coins in the same order (and should + * have the same length) in which the original age-withdraw request + * specified the respective denomination keys. + */ + const struct TALER_EXCHANGE_RevealedCoinInfo *coins; + + } ok; + /* FIXME[oec]: error cases */ + } details; + +}; + +typedef void +(*TALER_EXCHANGE_AgeWithdrawRevealCallback)( + void *cls, + const struct TALER_EXCHANGE_AgeWithdrawRevealResponse *awr); + +/** + * Submit an age-withdraw-reveal request to the exchange and get the exchange's + * response. + * + * This API is typically used by a wallet. Note that to ensure that + * no money is lost in case of hardware failures, the provided + * argument @a rd should be committed to persistent storage + * prior to calling this function. + * + * @param curl_ctx The curl context + * @param exchange_url The base url of the exchange + * @param reserve_priv The pivate key to the reserve + * @param num_coins The number of elements in @e coin_inputs + * @param coins_input The input for the coins to withdraw + * @param noreveal_index The index into each of the kappa coin candidates, that should not be revealed to the exchange + * @param h_commitment The commmitment from the previous call to /age-withdraw + * @param res_cb A callback for the result, maybe NULL + * @param res_cb_cls A closure for @e res_cb, maybe NULL + * @return a handle for this request; NULL if the argument was invalid. + * In this case, the callback will not be called. + */ +struct TALER_EXCHANGE_AgeWithdrawRevealHandle * +TALER_EXCHANGE_age_withdraw_reveal ( + struct GNUNET_CURL_Context *curl_ctx, + const char *exchange_url, + size_t num_coins, + const struct TALER_EXCHANGE_AgeWithdrawCoinInput coins_input[static + num_coins], + uint8_t noreveal_index, + const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, + TALER_EXCHANGE_AgeWithdrawRevealCallback res_cb, + void *res_cb_cls); + + +/** + * @brief Cancel an age-withdraw-reveal request + * + * @param awrh Handle to an age-withdraw-reqveal request + */ +void +TALER_EXCHANGE_age_withdraw_reveal_cancel ( + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh); + + /* ********************* /refresh/melt+reveal ***************************** */ diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 762a3f805..7a4febdd3 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -23,6 +23,7 @@ libtalerexchange_la_LDFLAGS = \ libtalerexchange_la_SOURCES = \ exchange_api_add_aml_decision.c \ exchange_api_age_withdraw.c \ + exchange_api_age_withdraw_reveal.c \ exchange_api_auditor_add_denomination.c \ exchange_api_batch_deposit.c \ exchange_api_batch_withdraw.c \ diff --git a/src/lib/exchange_api_age_withdraw.c b/src/lib/exchange_api_age_withdraw.c index 7bad025b2..f1d26c3cc 100644 --- a/src/lib/exchange_api_age_withdraw.c +++ b/src/lib/exchange_api_age_withdraw.c @@ -235,7 +235,8 @@ reserve_age_withdraw_ok ( { struct TALER_EXCHANGE_AgeWithdrawResponse response = { .hr.reply = j_response, - .hr.http_status = MHD_HTTP_OK + .hr.http_status = MHD_HTTP_OK, + .details.ok.h_commitment = awh->h_commitment }; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_uint8 ("noreaveal_index", @@ -568,12 +569,13 @@ perform_protocol ( /* Build the candidate array */ { - const struct CoinCandidate *can = awh->coin_data[i].coin_candidates; json_t *j_can = json_array (); FAIL_IF (NULL == j_can); for (size_t k = 0; k < TALER_CNC_KAPPA; k++) { + const struct CoinCandidate *can = + &awh->coin_data[i].coin_candidates[k]; json_t *jc = GNUNET_JSON_PACK ( TALER_JSON_pack_blinded_planchet ( NULL, @@ -591,20 +593,17 @@ perform_protocol ( } } + /* Build the hash of the commitment */ + GNUNET_CRYPTO_hash_context_finish (coins_hctx, + &awh->h_commitment.hash); + /* Sign the request */ - { - struct TALER_AgeWithdrawCommitmentHashP coins_commitment_h; - - GNUNET_CRYPTO_hash_context_finish (coins_hctx, - &coins_commitment_h.hash); - - TALER_wallet_age_withdraw_sign (&coins_commitment_h, - &awh->amount_with_fee, - &awh->age_mask, - awh->max_age, - awh->reserve_priv, - &awh->reserve_sig); - } + TALER_wallet_age_withdraw_sign (&awh->h_commitment, + &awh->amount_with_fee, + &awh->age_mask, + awh->max_age, + awh->reserve_priv, + &awh->reserve_sig); /* Initiate the POST-request */ j_request_body = GNUNET_JSON_PACK ( @@ -833,7 +832,7 @@ prepare_coins ( { struct CoinCandidate *can = &cd->coin_candidates[k]; - can->secret = input->secret[k]; + can->secret = input->secrets[k]; /* Derive the age restriction from the given secret and * the maximum age */ diff --git a/src/lib/exchange_api_age_withdraw_reveal.c b/src/lib/exchange_api_age_withdraw_reveal.c new file mode 100644 index 000000000..fdca3d6e0 --- /dev/null +++ b/src/lib/exchange_api_age_withdraw_reveal.c @@ -0,0 +1,435 @@ +/* + This file is part of TALER + Copyright (C) 2023 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_age_withdraw_reveal.c + * @brief Implementation of /age-withdraw/$ACH/reveal requests + * @author Özgür Kesim + */ + +#include "platform.h" +#include +#include +#include /* just for HTTP status codes */ +#include +#include +#include +#include "taler_curl_lib.h" +#include "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_common.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + +/** + * Handler for a running age-withdraw-reveal request + */ +struct TALER_EXCHANGE_AgeWithdrawRevealHandle +{ + + /* The index not to be disclosed */ + uint8_t noreveal_index; + + /* The age-withdraw commitment */ + struct TALER_AgeWithdrawCommitmentHashP h_commitment; + + /* Number of coins */ + size_t num_coins; + + /* The n*kappa coin secrets from the age-withdraw commitment */ + const struct TALER_EXCHANGE_AgeWithdrawCoinInput *coins_input; + + /* The curl context for the request */ + struct GNUNET_CURL_Context *curl_ctx; + + /* The url for the reveal request */ + const char *request_url; + + /** + * CURL handle for the request job. + */ + struct GNUNET_CURL_Job *job; + + /** + * Post Context + */ + struct TALER_CURL_PostContext post_ctx; + + /* Callback */ + TALER_EXCHANGE_AgeWithdrawRevealCallback callback; + + /* Reveal */ + void *callback_cls; +}; + + +/** + * We got a 200 OK response for the /age-withdraw/$ACH/reveal operation. + * Extract the signed blindedcoins and return it to the caller. + * + * @param awrh operation handle + * @param j_response reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +age_withdraw_reveal_ok ( + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh, + const json_t *j_response) +{ + struct TALER_EXCHANGE_AgeWithdrawRevealResponse response = { + .hr.reply = j_response, + .hr.http_status = MHD_HTTP_OK, + .details.ok.num_coins = awrh->num_coins + }; + 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 (awrh->num_coins != json_array_size (j_sigs)) + { + /* Number of coins generated does not match our expectation */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + for (size_t n = 0; n < awrh->num_coins; n++) + { + // TODO[oec] extract the individual coins. + } + + awrh->callback (awrh->callback_cls, + &response); + /* make sure the callback isn't called again */ + awrh->callback = NULL; + + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /age-withdraw/$ACH/reveal request. + * + * @param cls the `struct TALER_EXCHANGE_AgeWithdrawRevealHandle` + * @param response_code The HTTP response code + * @param response response data + */ +static void +handle_age_withdraw_reveal_finished ( + void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh = cls; + const json_t *j_response = response; + struct TALER_EXCHANGE_AgeWithdrawRevealResponse awr = { + .hr.reply = j_response, + .hr.http_status = (unsigned int) response_code + }; + + awrh->job = NULL; + switch (response_code) + { + case 0: + awr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + age_withdraw_reveal_ok (awrh, + j_response)) + { + GNUNET_break_op (0); + awr.hr.http_status = 0; + awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == awrh->callback); + TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); + return; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + /* only validate reply is well-formed */ + { + uint64_t ptu; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("legitimization_uuid", + &ptu), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + awr.hr.http_status = 0; + awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + 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 */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_FORBIDDEN: + GNUNET_break_op (0); + /** + * This should never happen, as we don't sent any signatures + * to the exchange to verify. We should simply pass the JSON reply + * to the application + **/ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.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 age-withraw commitment. */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_CONFLICT: + /* An age commitment for one of the coins did not fulfill + * the required maximum age requirement of the corresponding + * reserve. + * Error code: TALER_EC_EXCHANGE_GENERIC_COIN_AGE_REQUIREMENT_FAILURE. + */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.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 */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange age-withdraw\n", + (unsigned int) response_code, + (int) awr.hr.ec); + break; + } + awrh->callback (awrh->callback_cls, + &awr); + TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); +} + + +/** + * Prepares the request URL for the age-withdraw-reveal request + * + * @param exchange_url The base-URL to the exchange + * @param[in,out] awrh The handler + * @return GNUNET_OK on success, GNUNET_SYSERR otherwise + */ +static +enum GNUNET_GenericReturnValue +prepare_url ( + const char *exchange_url, + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh) +{ + char arg_str[sizeof (struct TALER_AgeWithdrawCommitmentHashP) * 2 + 32]; + char pub_str[sizeof (struct TALER_AgeWithdrawCommitmentHashP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string (&awrh->h_commitment, + sizeof (awrh->h_commitment), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "age-withraw/%s/reveal", + pub_str); + + awrh->request_url = TALER_url_join (exchange_url, + arg_str, + NULL); + if (NULL == awrh->request_url) + { + GNUNET_break (0); + TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * Call /age-withdraw/$ACH/reveal + * + * @param awrh The handler + * @param num_coins Number of coin candidates in reveal_inputs + * @param reveal_inputs The secrets of the coin candidates + */ +static +void +perform_protocol ( + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh) +{ + CURL *curlh = NULL; + json_t *j_request_body = NULL; + json_t *j_array_of_secrets = NULL; + json_t *j_secrets = NULL; + json_t *j_sec = NULL; + +#define FAIL_IF(cond) \ + do { \ + if ((cond)) \ + { \ + GNUNET_break (! (cond)); \ + goto ERROR; \ + } \ + } while(0) + + for (size_t n = 0; n < awrh->num_coins; n++) + { + const struct TALER_PlanchetMasterSecretP *secrets = + awrh->coins_input[n].secrets; + + j_secrets = json_array (); + FAIL_IF (NULL == j_secrets); + + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + const struct TALER_PlanchetMasterSecretP *secret = &secrets[k]; + if (awrh->noreveal_index == k) + continue; + + j_sec = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto (NULL, secret)); + + FAIL_IF (NULL == j_sec); + FAIL_IF (0 < json_array_append_new (j_secrets, + j_sec)); + } + + FAIL_IF (0 < json_array_append_new (j_array_of_secrets, + j_secrets)); + } + j_request_body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("disclosed_coin_secrets", + j_array_of_secrets)); + FAIL_IF (NULL == j_request_body); + + curlh = TALER_EXCHANGE_curl_easy_get_ (awrh->request_url); + FAIL_IF (NULL == curlh); + FAIL_IF (GNUNET_OK != + TALER_curl_easy_post (&awrh->post_ctx, + curlh, + j_request_body)); + json_decref (j_request_body); + j_request_body = NULL; + + awrh->job = GNUNET_CURL_job_add2 (awrh->curl_ctx, + curlh, + awrh->post_ctx.headers, + &handle_age_withdraw_reveal_finished, + awrh); + FAIL_IF (NULL == awrh->job); + + /* No error, return */ + return; + +ERROR: + if (NULL != j_sec) + json_decref (j_sec); + if (NULL != j_secrets) + json_decref (j_secrets); + if (NULL != j_array_of_secrets) + json_decref (j_array_of_secrets); + if (NULL != j_request_body) + json_decref (j_request_body); + if (NULL != curlh) + curl_easy_cleanup (curlh); + TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); + return; +#undef FAIL_IF +} + + +struct TALER_EXCHANGE_AgeWithdrawRevealHandle * +TALER_EXCHANGE_age_withdraw_reveal ( + struct GNUNET_CURL_Context *curl_ctx, + const char *exchange_url, + size_t num_coins, + const struct TALER_EXCHANGE_AgeWithdrawCoinInput coins_input[static + num_coins], + uint8_t noreveal_index, + const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, + TALER_EXCHANGE_AgeWithdrawRevealCallback reveal_cb, + void *reveal_cb_cls) +{ + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh = + GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawRevealHandle); + awrh->curl_ctx = curl_ctx; + awrh->noreveal_index = noreveal_index; + awrh->callback = reveal_cb; + awrh->callback_cls = reveal_cb_cls; + awrh->h_commitment = *h_commitment; + awrh->num_coins = num_coins; + awrh->coins_input = coins_input; + + + if (GNUNET_OK != + prepare_url (exchange_url, + awrh)) + return NULL; + + perform_protocol (awrh); + + return awrh; +} + + +void +TALER_EXCHANGE_age_withdraw_reveal_cancel ( + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh) +{ + /* FIXME[oec] */ + (void) awrh; +} + + +/* exchange_api_age_withdraw_reveal.c */