diff options
Diffstat (limited to 'src/lib')
72 files changed, 19445 insertions, 7984 deletions
diff --git a/src/lib/.gitignore b/src/lib/.gitignore new file mode 100644 index 000000000..6664876f2 --- /dev/null +++ b/src/lib/.gitignore @@ -0,0 +1 @@ +test_stefan diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 22209de45..63dab7c80 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -18,45 +18,77 @@ lib_LTLIBRARIES = \ libtalerexchange.la libtalerexchange_la_LDFLAGS = \ - -version-info 4:0:0 \ + -version-info 7:0:0 \ -no-undefined 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 \ + exchange_api_batch_withdraw2.c \ exchange_api_curl_defaults.c exchange_api_curl_defaults.h \ - exchange_api_common.c \ + exchange_api_coins_history.c \ + exchange_api_common.c exchange_api_common.h \ + exchange_api_contracts_get.c \ + exchange_api_csr_melt.c \ + exchange_api_csr_withdraw.c \ exchange_api_handle.c exchange_api_handle.h \ - exchange_api_deposit.c \ exchange_api_deposits_get.c \ + exchange_api_kyc_check.c \ + exchange_api_kyc_proof.c \ + exchange_api_kyc_wallet.c \ exchange_api_link.c \ + exchange_api_lookup_aml_decision.c \ + exchange_api_lookup_aml_decisions.c \ + exchange_api_management_add_partner.c \ exchange_api_management_auditor_disable.c \ exchange_api_management_auditor_enable.c \ + exchange_api_management_drain_profits.c \ exchange_api_management_get_keys.c \ exchange_api_management_post_keys.c \ + exchange_api_management_post_extensions.c \ exchange_api_management_revoke_denomination_key.c \ exchange_api_management_revoke_signing_key.c \ + exchange_api_management_set_global_fee.c \ exchange_api_management_set_wire_fee.c \ + exchange_api_management_update_aml_officer.c \ exchange_api_management_wire_disable.c \ exchange_api_management_wire_enable.c \ exchange_api_melt.c \ + exchange_api_purse_create_with_deposit.c \ + exchange_api_purse_create_with_merge.c \ + exchange_api_purse_delete.c \ + exchange_api_purse_deposit.c \ + exchange_api_purse_merge.c \ + exchange_api_purses_get.c \ exchange_api_recoup.c \ + exchange_api_recoup_refresh.c \ exchange_api_refresh_common.c exchange_api_refresh_common.h \ exchange_api_refreshes_reveal.c \ exchange_api_refund.c \ + exchange_api_reserves_attest.c \ + exchange_api_reserves_close.c \ exchange_api_reserves_get.c \ - exchange_api_transfers_get.c \ - exchange_api_withdraw.c \ - exchange_api_withdraw2.c \ - exchange_api_wire.c + exchange_api_reserves_get_attestable.c \ + exchange_api_reserves_history.c \ + exchange_api_reserves_open.c \ + exchange_api_stefan.c \ + exchange_api_transfers_get.c libtalerexchange_la_LIBADD = \ libtalerauditor.la \ $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/kyclogic/libtalerkyclogic.la \ $(top_builddir)/src/curl/libtalercurl.la \ $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/extensions/libtalerextensions.la \ -lgnunetcurl \ -lgnunetjson \ -lgnunetutil \ -ljansson \ - $(LIBGNURLCURL_LIBS) \ + -lcurl \ + -lm \ $(XLIB) libtalerauditor_la_LDFLAGS = \ @@ -64,9 +96,8 @@ libtalerauditor_la_LDFLAGS = \ -no-undefined libtalerauditor_la_SOURCES = \ auditor_api_curl_defaults.c auditor_api_curl_defaults.h \ - auditor_api_handle.c auditor_api_handle.h \ - auditor_api_deposit_confirmation.c \ - auditor_api_exchanges.c + auditor_api_get_config.c \ + auditor_api_deposit_confirmation.c libtalerauditor_la_LIBADD = \ $(top_builddir)/src/curl/libtalercurl.la \ $(top_builddir)/src/json/libtalerjson.la \ @@ -75,5 +106,21 @@ libtalerauditor_la_LIBADD = \ -lgnunetjson \ -lgnunetutil \ -ljansson \ - $(LIBGNURLCURL_LIBS) \ + -lcurl \ + -lm \ $(XLIB) + + +check_PROGRAMS = \ + test_stefan + +TESTS = \ + $(check_PROGRAMS) + + +test_stefan_SOURCES = \ + test_stefan.c +test_stefan_LDADD = \ + $(top_builddir)/src/lib/libtalerexchange.la \ + $(top_builddir)/src/util/libtalerutil.la \ + -lgnunetutil diff --git a/src/lib/auditor_api_curl_defaults.c b/src/lib/auditor_api_curl_defaults.c index d8c6f619c..972f28ca6 100644 --- a/src/lib/auditor_api_curl_defaults.c +++ b/src/lib/auditor_api_curl_defaults.c @@ -19,15 +19,11 @@ * @brief curl easy handle defaults * @author Florian Dold */ +#include "platform.h" +#include "taler_curl_lib.h" #include "auditor_api_curl_defaults.h" -/** - * Get a curl handle with the right defaults - * for the exchange lib. In the future, we might manage a pool of connections here. - * - * @param url URL to query - */ CURL * TALER_AUDITOR_curl_easy_get_ (const char *url) { @@ -43,16 +39,14 @@ TALER_AUDITOR_curl_easy_get_ (const char *url) curl_easy_setopt (eh, CURLOPT_URL, url)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_FOLLOWLOCATION, - 1L)); - /* limit MAXREDIRS to 5 as a simple security measure against - a potential infinite loop caused by a malicious target */ - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_MAXREDIRS, - 5L)); + TALER_curl_set_secure_redirect_policy (eh, + url); + /* Enable compression (using whatever curl likes), see + https://curl.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html */ + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_ACCEPT_ENCODING, + "")); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_TCP_FASTOPEN, diff --git a/src/lib/auditor_api_deposit_confirmation.c b/src/lib/auditor_api_deposit_confirmation.c index 9c13c3f0f..172a12ece 100644 --- a/src/lib/auditor_api_deposit_confirmation.c +++ b/src/lib/auditor_api_deposit_confirmation.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2021 Taler Systems SA + Copyright (C) 2014-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 @@ -25,9 +25,10 @@ #include <gnunet/gnunet_util_lib.h> #include <gnunet/gnunet_json_lib.h> #include <gnunet/gnunet_curl_lib.h> +#include "taler_util.h" +#include "taler_curl_lib.h" #include "taler_json_lib.h" #include "taler_auditor_service.h" -#include "auditor_api_handle.h" #include "taler_signatures.h" #include "auditor_api_curl_defaults.h" @@ -39,11 +40,6 @@ struct TALER_AUDITOR_DepositConfirmationHandle { /** - * The connection to auditor this request handle will use - */ - struct TALER_AUDITOR_Handle *auditor; - - /** * The url for this request. */ char *url; @@ -87,64 +83,64 @@ handle_deposit_confirmation_finished (void *cls, { const json_t *json = djson; struct TALER_AUDITOR_DepositConfirmationHandle *dh = cls; - struct TALER_AUDITOR_HttpResponse hr = { - .reply = json, - .http_status = (unsigned int) response_code + struct TALER_AUDITOR_DepositConfirmationResponse dcr = { + .hr.reply = json, + .hr.http_status = (unsigned int) response_code }; dh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + dcr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: - hr.ec = TALER_EC_NONE; + dcr.hr.ec = TALER_EC_NONE; break; case MHD_HTTP_BAD_REQUEST: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + dcr.hr.ec = TALER_JSON_get_error_code (json); + dcr.hr.hint = TALER_JSON_get_error_hint (json); /* This should never happen, either us or the auditor is buggy (or API version conflict); just pass JSON reply to the application */ break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + dcr.hr.ec = TALER_JSON_get_error_code (json); + dcr.hr.hint = TALER_JSON_get_error_hint (json); /* Nothing really to verify, auditor says one of the signatures is invalid; as we checked them, this should never happen, we should pass the JSON reply to the application */ break; case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + dcr.hr.ec = TALER_JSON_get_error_code (json); + dcr.hr.hint = TALER_JSON_get_error_hint (json); /* Nothing really to verify, this should never happen, we should pass the JSON reply to the application */ break; case MHD_HTTP_GONE: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + dcr.hr.ec = TALER_JSON_get_error_code (json); + dcr.hr.hint = TALER_JSON_get_error_hint (json); /* Nothing really to verify, auditor says one of the signatures is invalid; as we checked them, this should never happen, we should pass the JSON reply to the application */ break; case MHD_HTTP_INTERNAL_SERVER_ERROR: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + dcr.hr.ec = TALER_JSON_get_error_code (json); + dcr.hr.hint = TALER_JSON_get_error_hint (json); /* Server had an internal issue; we should retry, but this API leaves this to the application */ break; default: /* unexpected response code */ - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + dcr.hr.ec = TALER_JSON_get_error_code (json); + dcr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for auditor deposit confirmation\n", (unsigned int) response_code, - hr.ec); + dcr.hr.ec); break; } dh->cb (dh->cb_cls, - &hr); + &dcr); TALER_AUDITOR_deposit_confirmation_cancel (dh); } @@ -153,11 +149,14 @@ handle_deposit_confirmation_finished (void *cls, * Verify signature information about the deposit-confirmation. * * @param h_wire hash of merchant wire details + * @param h_policy hash over the policy extension, if any * @param h_contract_terms hash of the contact of the merchant with the customer (further details are never disclosed to the auditor) * @param exchange_timestamp timestamp when the deposit was received by the wallet + * @param wire_deadline by what time must the amount be wired to the merchant * @param refund_deadline date until which the merchant can issue a refund to the customer via the auditor (can be zero if refunds are not allowed); must not be after the @a wire_deadline * @param amount_without_fee the amount confirmed to be wired by the exchange to the merchant - * @param coin_pub coin’s public key + * @param num_coins number of coins involved + * @param coin_sigs array of @a num_coins coin signatures * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) * @param exchange_sig the signature made with purpose #TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT * @param exchange_pub the public key of the exchange that matches @a exchange_sig @@ -168,52 +167,52 @@ handle_deposit_confirmation_finished (void *cls, * @param master_sig master signature affirming validity of @a exchange_pub * @return #GNUNET_OK if signatures are OK, #GNUNET_SYSERR if not */ -static int -verify_signatures (const struct GNUNET_HashCode *h_wire, - const struct GNUNET_HashCode *h_contract_terms, - struct GNUNET_TIME_Absolute exchange_timestamp, - struct GNUNET_TIME_Absolute refund_deadline, - const struct TALER_Amount *amount_without_fee, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_ExchangeSignatureP *exchange_sig, - const struct TALER_MasterPublicKeyP *master_pub, - struct GNUNET_TIME_Absolute ep_start, - struct GNUNET_TIME_Absolute ep_expire, - struct GNUNET_TIME_Absolute ep_end, - const struct TALER_MasterSignatureP *master_sig) +static enum GNUNET_GenericReturnValue +verify_signatures ( + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_ExtensionPolicyHashP *h_policy, + const struct TALER_PrivateContractHashP *h_contract_terms, + struct GNUNET_TIME_Timestamp exchange_timestamp, + struct GNUNET_TIME_Timestamp wire_deadline, + struct GNUNET_TIME_Timestamp refund_deadline, + const struct TALER_Amount *amount_without_fee, + unsigned int num_coins, + const struct TALER_CoinSpendSignatureP *coin_sigs[ + static num_coins], + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig, + const struct TALER_MasterPublicKeyP *master_pub, + struct GNUNET_TIME_Timestamp ep_start, + struct GNUNET_TIME_Timestamp ep_expire, + struct GNUNET_TIME_Timestamp ep_end, + const struct TALER_MasterSignatureP *master_sig) { + if (GNUNET_OK != + TALER_exchange_online_deposit_confirmation_verify ( + h_contract_terms, + h_wire, + h_policy, + exchange_timestamp, + wire_deadline, + refund_deadline, + amount_without_fee, + num_coins, + coin_sigs, + merchant_pub, + exchange_pub, + exchange_sig)) { - struct TALER_DepositConfirmationPS dc = { - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT), - .purpose.size = htonl (sizeof (dc)), - .h_contract_terms = *h_contract_terms, - .h_wire = *h_wire, - .exchange_timestamp = GNUNET_TIME_absolute_hton (exchange_timestamp), - .refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline), - .coin_pub = *coin_pub, - .merchant = *merchant_pub - }; - - TALER_amount_hton (&dc.amount_without_fee, - amount_without_fee); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT, - &dc, - &exchange_sig->eddsa_signature, - &exchange_pub->eddsa_pub)) + GNUNET_break_op (0); + TALER_LOG_WARNING ( + "Invalid signature on /deposit-confirmation request!\n"); { - GNUNET_break_op (0); - TALER_LOG_WARNING ( - "Invalid signature on /deposit-confirmation request!\n"); - { - TALER_LOG_DEBUG ("... amount_without_fee was %s\n", - TALER_amount2s (amount_without_fee)); - } - return GNUNET_SYSERR; + TALER_LOG_DEBUG ("... amount_without_fee was %s\n", + TALER_amount2s (amount_without_fee)); } + return GNUNET_SYSERR; } + if (GNUNET_OK != TALER_exchange_offline_signkey_validity_verify ( exchange_pub, @@ -227,7 +226,7 @@ verify_signatures (const struct GNUNET_HashCode *h_wire, TALER_LOG_WARNING ("Invalid signature on exchange signing key!\n"); return GNUNET_SYSERR; } - if (GNUNET_TIME_absolute_is_past (ep_end)) + if (GNUNET_TIME_absolute_is_past (ep_end.abs_time)) { GNUNET_break (0); TALER_LOG_WARNING ("Exchange signing key is no longer valid!\n"); @@ -237,78 +236,54 @@ verify_signatures (const struct GNUNET_HashCode *h_wire, } -/** - * Submit a deposit-confirmation permission to the auditor and get the - * auditor's response. Note that while we return the response - * verbatim to the caller for further processing, we do already verify - * that the response is well-formed. If the auditor's reply is not - * well-formed, we return an HTTP status code of zero to @a cb. - * - * We also verify that the @a exchange_sig is valid for this deposit-confirmation - * request, and that the @a master_sig is a valid signature for @a - * exchange_pub. Also, the @a auditor must be ready to operate (i.e. have - * finished processing the /version reply). If either check fails, we do - * NOT initiate the transaction with the auditor and instead return NULL. - * - * @param auditor the auditor handle; the auditor must be ready to operate - * @param h_wire hash of merchant wire details - * @param h_contract_terms hash of the contact of the merchant with the customer (further details are never disclosed to the auditor) - * @param exchange_timestamp timestamp when deposit was received by the exchange - * @param refund_deadline date until which the merchant can issue a refund to the customer via the auditor (can be zero if refunds are not allowed); must not be after the @a wire_deadline - * @param amount_without_fee the amount confirmed to be wired by the exchange to the merchant - * @param coin_pub coin’s public key - * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) - * @param exchange_sig the signature made with purpose #TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT - * @param exchange_pub the public key of the exchange that matches @a exchange_sig - * @param master_pub master public key of the exchange - * @param ep_start when does @a exchange_pub validity start - * @param ep_expire when does @a exchange_pub usage end - * @param ep_end when does @a exchange_pub legal validity end - * @param master_sig master signature affirming validity of @a exchange_pub - * @param cb the callback to call when a reply for this request is available - * @param cb_cls closure for the above callback - * @return a handle for this request; NULL if the inputs are invalid (i.e. - * signatures fail to verify). In this case, the callback is not called. - */ struct TALER_AUDITOR_DepositConfirmationHandle * TALER_AUDITOR_deposit_confirmation ( - struct TALER_AUDITOR_Handle *auditor, - const struct GNUNET_HashCode *h_wire, - const struct GNUNET_HashCode *h_contract_terms, - struct GNUNET_TIME_Absolute exchange_timestamp, - struct GNUNET_TIME_Absolute refund_deadline, - const struct TALER_Amount *amount_without_fee, - const struct TALER_CoinSpendPublicKeyP *coin_pub, + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_ExtensionPolicyHashP *h_policy, + const struct TALER_PrivateContractHashP *h_contract_terms, + struct GNUNET_TIME_Timestamp exchange_timestamp, + struct GNUNET_TIME_Timestamp wire_deadline, + struct GNUNET_TIME_Timestamp refund_deadline, + const struct TALER_Amount *total_without_fee, + unsigned int num_coins, + const struct TALER_CoinSpendPublicKeyP *coin_pubs[ + static num_coins], + const struct TALER_CoinSpendSignatureP *coin_sigs[ + static num_coins], const struct TALER_MerchantPublicKeyP *merchant_pub, const struct TALER_ExchangePublicKeyP *exchange_pub, const struct TALER_ExchangeSignatureP *exchange_sig, const struct TALER_MasterPublicKeyP *master_pub, - struct GNUNET_TIME_Absolute ep_start, - struct GNUNET_TIME_Absolute ep_expire, - struct GNUNET_TIME_Absolute ep_end, + struct GNUNET_TIME_Timestamp ep_start, + struct GNUNET_TIME_Timestamp ep_expire, + struct GNUNET_TIME_Timestamp ep_end, const struct TALER_MasterSignatureP *master_sig, TALER_AUDITOR_DepositConfirmationResultCallback cb, void *cb_cls) { struct TALER_AUDITOR_DepositConfirmationHandle *dh; - struct GNUNET_CURL_Context *ctx; json_t *deposit_confirmation_obj; CURL *eh; + json_t *jcoin_sigs; + json_t *jcoin_pubs; - (void) GNUNET_TIME_round_abs (&exchange_timestamp); - (void) GNUNET_TIME_round_abs (&refund_deadline); - (void) GNUNET_TIME_round_abs (&ep_start); - (void) GNUNET_TIME_round_abs (&ep_expire); - (void) GNUNET_TIME_round_abs (&ep_end); - GNUNET_assert (GNUNET_YES == - TALER_AUDITOR_handle_is_ready_ (auditor)); + if (0 == num_coins) + { + GNUNET_break (0); + return NULL; + } if (GNUNET_OK != verify_signatures (h_wire, + h_policy, h_contract_terms, exchange_timestamp, + wire_deadline, refund_deadline, - amount_without_fee, - coin_pub, + total_without_fee, + num_coins, + coin_sigs, merchant_pub, exchange_pub, exchange_sig, @@ -321,50 +296,70 @@ TALER_AUDITOR_deposit_confirmation ( GNUNET_break_op (0); return NULL; } - + jcoin_sigs = json_array (); + GNUNET_assert (NULL != jcoin_sigs); + jcoin_pubs = json_array (); + GNUNET_assert (NULL != jcoin_pubs); + for (unsigned int i = 0; i<num_coins; i++) + { + GNUNET_assert (0 == + json_array_append_new (jcoin_sigs, + GNUNET_JSON_from_data_auto ( + coin_sigs[i]))); + GNUNET_assert (0 == + json_array_append_new (jcoin_pubs, + GNUNET_JSON_from_data_auto ( + coin_pubs[i]))); + } deposit_confirmation_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("h_wire", h_wire), + GNUNET_JSON_pack_data_auto ("h_policy", + h_policy), GNUNET_JSON_pack_data_auto ("h_contract_terms", h_contract_terms), - GNUNET_JSON_pack_time_abs ("exchange_timestamp", - exchange_timestamp), - GNUNET_JSON_pack_time_abs ("refund_deadline", - refund_deadline), - TALER_JSON_pack_amount ("amount_without_fee", - amount_without_fee), - GNUNET_JSON_pack_data_auto ("coin_pub", - coin_pub), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + exchange_timestamp), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("refund_deadline", + refund_deadline)), + GNUNET_JSON_pack_timestamp ("wire_deadline", + wire_deadline), + TALER_JSON_pack_amount ("total_without_fee", + total_without_fee), + GNUNET_JSON_pack_array_steal ("coin_pubs", + jcoin_pubs), + GNUNET_JSON_pack_array_steal ("coin_sigs", + jcoin_sigs), GNUNET_JSON_pack_data_auto ("merchant_pub", merchant_pub), GNUNET_JSON_pack_data_auto ("exchange_sig", exchange_sig), GNUNET_JSON_pack_data_auto ("master_pub", master_pub), - GNUNET_JSON_pack_time_abs ("ep_start", - ep_start), - GNUNET_JSON_pack_time_abs ("ep_expire", - ep_expire), - GNUNET_JSON_pack_time_abs ("ep_end", - ep_end), + GNUNET_JSON_pack_timestamp ("ep_start", + ep_start), + GNUNET_JSON_pack_timestamp ("ep_expire", + ep_expire), + GNUNET_JSON_pack_timestamp ("ep_end", + ep_end), GNUNET_JSON_pack_data_auto ("master_sig", master_sig), GNUNET_JSON_pack_data_auto ("exchange_pub", exchange_pub)); dh = GNUNET_new (struct TALER_AUDITOR_DepositConfirmationHandle); - dh->auditor = auditor; dh->cb = cb; dh->cb_cls = cb_cls; - dh->url = TALER_AUDITOR_path_to_url_ (auditor, - "/deposit-confirmation"); + dh->url = TALER_url_join (url, + "deposit-confirmation", + NULL); if (NULL == dh->url) { GNUNET_free (dh); return NULL; } eh = TALER_AUDITOR_curl_easy_get_ (dh->url); - if ( (NULL == eh) || (CURLE_OK != curl_easy_setopt (eh, @@ -387,22 +382,25 @@ TALER_AUDITOR_deposit_confirmation ( GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "URL for deposit-confirmation: `%s'\n", dh->url); - ctx = TALER_AUDITOR_handle_to_context_ (auditor); dh->job = GNUNET_CURL_job_add2 (ctx, eh, dh->ctx.headers, &handle_deposit_confirmation_finished, dh); + { + /* Disable 100 continue processing */ + struct curl_slist *x_headers; + + x_headers = curl_slist_append (NULL, + "Expect:"); + GNUNET_CURL_extend_headers (dh->job, + x_headers); + curl_slist_free_all (x_headers); + } return dh; } -/** - * Cancel a deposit-confirmation permission request. This function cannot be used - * on a request handle if a response is already served for it. - * - * @param deposit_confirmation the deposit-confirmation permission request handle - */ void TALER_AUDITOR_deposit_confirmation_cancel ( struct TALER_AUDITOR_DepositConfirmationHandle *deposit_confirmation) diff --git a/src/lib/auditor_api_exchanges.c b/src/lib/auditor_api_exchanges.c deleted file mode 100644 index 0fe4ce287..000000000 --- a/src/lib/auditor_api_exchanges.c +++ /dev/null @@ -1,269 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2018 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/auditor_api_exchanges.c - * @brief Implementation of the /exchanges request of the auditor's HTTP API - * @author Christian Grothoff - */ -#include "platform.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 "taler_json_lib.h" -#include "taler_auditor_service.h" -#include "auditor_api_handle.h" -#include "taler_signatures.h" -#include "auditor_api_curl_defaults.h" - -/** - * How many exchanges do we allow a single auditor to - * audit at most? - */ -#define MAX_EXCHANGES 1024 - - -/** - * @brief A ListExchanges Handle - */ -struct TALER_AUDITOR_ListExchangesHandle -{ - - /** - * The connection to auditor this request handle will use - */ - struct TALER_AUDITOR_Handle *auditor; - - /** - * The url for this request. - */ - char *url; - - /** - * Handle for the request. - */ - struct GNUNET_CURL_Job *job; - - /** - * Function to call with the result. - */ - TALER_AUDITOR_ListExchangesResultCallback cb; - - /** - * Closure for @a cb. - */ - void *cb_cls; - -}; - - -/** - * Function called when we're done processing the - * HTTP /exchanges request. - * - * @param cls the `struct TALER_AUDITOR_ListExchangesHandle` - * @param response_code HTTP response code, 0 on error - * @param djson parsed JSON result, NULL on error - */ -static void -handle_exchanges_finished (void *cls, - long response_code, - const void *djson) -{ - const json_t *json = djson; - const json_t *ja; - unsigned int ja_len; - struct TALER_AUDITOR_ListExchangesHandle *leh = cls; - struct TALER_AUDITOR_HttpResponse hr = { - .reply = json, - .http_status = (unsigned int) response_code - }; - - leh->job = NULL; - switch (response_code) - { - case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - ja = json_object_get (json, - "exchanges"); - if ( (NULL == ja) || - (! json_is_array (ja)) ) - { - GNUNET_break (0); - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - hr.http_status = 0; - break; - } - - ja_len = json_array_size (ja); - if (ja_len > MAX_EXCHANGES) - { - GNUNET_break (0); - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - hr.http_status = 0; - break; - } - { - struct TALER_AUDITOR_ExchangeInfo ei[ja_len]; - int ok; - - ok = GNUNET_YES; - for (unsigned int i = 0; i<ja_len; i++) - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_pub", &ei[i].master_pub), - GNUNET_JSON_spec_string ("exchange_url", &ei[i].exchange_url), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (json_array_get (ja, - i), - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - ok = GNUNET_NO; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - hr.http_status = 0; - break; - } - } - if (GNUNET_YES != ok) - break; - leh->cb (leh->cb_cls, - &hr, - ja_len, - ei); - TALER_AUDITOR_list_exchanges_cancel (leh); - return; - } - case MHD_HTTP_BAD_REQUEST: - /* This should never happen, either us or the auditor is buggy - (or API version conflict); just pass JSON reply to the application */ - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); - break; - case MHD_HTTP_NOT_FOUND: - /* Nothing really to verify, this should never - happen, we should pass the JSON reply to the application */ - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); - 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 (json); - hr.hint = TALER_JSON_get_error_hint (json); - break; - default: - /* unexpected response code */ - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for auditor list-exchanges request\n", - (unsigned int) response_code, - (int) hr.ec); - GNUNET_break_op (0); - break; - } - if (NULL != leh->cb) - leh->cb (leh->cb_cls, - &hr, - 0, - NULL); - TALER_AUDITOR_list_exchanges_cancel (leh); -} - - -/** - * Submit an /exchanges request to the auditor and get the - * auditor's response. If the auditor's reply is not - * well-formed, we return an HTTP status code of zero to @a cb. - * - * @param auditor the auditor handle; the auditor must be ready to operate - * @param cb the callback to call when a reply for this request is available - * @param cb_cls closure for the above callback - * @return a handle for this request; NULL if the inputs are invalid (i.e. - * signatures fail to verify). In this case, the callback is not called. - */ -struct TALER_AUDITOR_ListExchangesHandle * -TALER_AUDITOR_list_exchanges (struct TALER_AUDITOR_Handle *auditor, - TALER_AUDITOR_ListExchangesResultCallback cb, - void *cb_cls) -{ - struct TALER_AUDITOR_ListExchangesHandle *leh; - struct GNUNET_CURL_Context *ctx; - CURL *eh; - - GNUNET_assert (GNUNET_YES == - TALER_AUDITOR_handle_is_ready_ (auditor)); - - leh = GNUNET_new (struct TALER_AUDITOR_ListExchangesHandle); - leh->auditor = auditor; - leh->cb = cb; - leh->cb_cls = cb_cls; - leh->url = TALER_AUDITOR_path_to_url_ (auditor, "/exchanges"); - if (NULL == leh->url) - { - GNUNET_free (leh); - return NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "URL for list-exchanges: `%s'\n", - leh->url); - eh = TALER_AUDITOR_curl_easy_get_ (leh->url); - if (NULL == eh) - { - GNUNET_break (0); - GNUNET_free (leh->url); - GNUNET_free (leh); - return NULL; - } - ctx = TALER_AUDITOR_handle_to_context_ (auditor); - leh->job = GNUNET_CURL_job_add (ctx, - eh, - &handle_exchanges_finished, - leh); - return leh; -} - - -/** - * Cancel a list exchanges request. This function cannot be used - * on a request handle if a response is already served for it. - * - * @param leh the list exchanges request handle - */ -void -TALER_AUDITOR_list_exchanges_cancel (struct - TALER_AUDITOR_ListExchangesHandle *leh) -{ - if (NULL != leh->job) - { - GNUNET_CURL_job_cancel (leh->job); - leh->job = NULL; - } - GNUNET_free (leh->url); - GNUNET_free (leh); -} - - -/* end of auditor_api_exchanges.c */ diff --git a/src/lib/auditor_api_get_config.c b/src/lib/auditor_api_get_config.c new file mode 100644 index 000000000..1e8e0bb30 --- /dev/null +++ b/src/lib/auditor_api_get_config.c @@ -0,0 +1,278 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/auditor_api_get_config.c + * @brief Implementation of /config for the auditor's HTTP API + * @author Sree Harsha Totakura <sreeharsha@totakura.in> + * @author Christian Grothoff + */ +#include "platform.h" +#include <microhttpd.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_json_lib.h" +#include "taler_auditor_service.h" +#include "taler_signatures.h" +#include "auditor_api_curl_defaults.h" + + +/** + * Which revision of the Taler auditor protocol is implemented + * by this library? Used to determine compatibility. + */ +#define TALER_PROTOCOL_CURRENT 1 + +/** + * How many revisions back are we compatible to? + */ +#define TALER_PROTOCOL_AGE 0 + + +/** + * Log error related to CURL operations. + * + * @param type log level + * @param function which function failed to run + * @param code what was the curl error code + */ +#define CURL_STRERROR(type, function, code) \ + GNUNET_log (type, "Curl function `%s' has failed at `%s:%d' with error: %s", \ + function, __FILE__, __LINE__, curl_easy_strerror (code)); + + +/** + * Handle for the get config request. + */ +struct TALER_AUDITOR_GetConfigHandle +{ + /** + * The context of this handle + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Function to call with the auditor's certification data, + * NULL if this has already been done. + */ + TALER_AUDITOR_ConfigCallback config_cb; + + /** + * Closure to pass to @e config_cb. + */ + void *config_cb_cls; + + /** + * Data for the request to get the /config of a auditor, + * NULL once we are past stage #MHS_INIT. + */ + struct GNUNET_CURL_Job *vr; + + /** + * The url for the @e vr job. + */ + char *vr_url; + +}; + + +/* ***************** Internal /config fetching ************* */ + +/** + * Decode the JSON in @a resp_obj from the /config response and store the data + * in the @a key_data. + * + * @param[in] resp_obj JSON object to parse + * @param[in,out] vi where to store the results we decoded + * @param[out] vc where to store config compatibility data + * @return #TALER_EC_NONE on success + */ +static enum TALER_ErrorCode +decode_config_json (const json_t *resp_obj, + struct TALER_AUDITOR_ConfigInformation *vi, + enum TALER_AUDITOR_VersionCompatibility *vc) +{ + struct TALER_JSON_ProtocolVersion pv; + const char *ver; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_version ("version", + &pv), + GNUNET_JSON_spec_string ("version", + &ver), + GNUNET_JSON_spec_fixed_auto ("exchange_master_public_key", + &vi->exchange_master_public_key), + GNUNET_JSON_spec_fixed_auto ("auditor_public_key", + &vi->auditor_pub), + GNUNET_JSON_spec_end () + }; + + if (JSON_OBJECT != json_typeof (resp_obj)) + { + GNUNET_break_op (0); + return TALER_EC_GENERIC_JSON_INVALID; + } + /* check the config */ + if (GNUNET_OK != + GNUNET_JSON_parse (resp_obj, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return TALER_EC_GENERIC_JSON_INVALID; + } + vi->version = ver; + *vc = TALER_AUDITOR_VC_MATCH; + if (TALER_PROTOCOL_CURRENT < pv.current) + { + *vc |= TALER_AUDITOR_VC_NEWER; + if (TALER_PROTOCOL_CURRENT < pv.current - pv.age) + *vc |= TALER_AUDITOR_VC_INCOMPATIBLE; + } + if (TALER_PROTOCOL_CURRENT > pv.current) + { + *vc |= TALER_AUDITOR_VC_OLDER; + if (TALER_PROTOCOL_CURRENT - TALER_PROTOCOL_AGE > pv.current) + *vc |= TALER_AUDITOR_VC_INCOMPATIBLE; + } + return TALER_EC_NONE; +} + + +/** + * Callback used when downloading the reply to a /config request + * is complete. + * + * @param cls the `struct TALER_AUDITOR_GetConfigHandle` + * @param response_code HTTP response code, 0 on error + * @param gresp_obj parsed JSON result, NULL on error, must be a `const json_t *` + */ +static void +config_completed_cb (void *cls, + long response_code, + const void *gresp_obj) +{ + struct TALER_AUDITOR_GetConfigHandle *auditor = cls; + const json_t *resp_obj = gresp_obj; + struct TALER_AUDITOR_ConfigResponse vr = { + .hr.reply = resp_obj, + .hr.http_status = (unsigned int) response_code + }; + + auditor->vr = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Received config from URL `%s' with status %ld.\n", + auditor->vr_url, + response_code); + switch (response_code) + { + case 0: + GNUNET_break_op (0); + vr.hr.ec = TALER_EC_INVALID; + break; + case MHD_HTTP_OK: + if (NULL == resp_obj) + { + GNUNET_break_op (0); + vr.hr.http_status = 0; + vr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + vr.hr.ec = decode_config_json (resp_obj, + &vr.details.ok.vi, + &vr.details.ok.compat); + if (TALER_EC_NONE != vr.hr.ec) + { + GNUNET_break_op (0); + vr.hr.http_status = 0; + break; + } + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + vr.hr.ec = TALER_JSON_get_error_code (resp_obj); + vr.hr.hint = TALER_JSON_get_error_hint (resp_obj); + break; + default: + vr.hr.ec = TALER_JSON_get_error_code (resp_obj); + vr.hr.hint = TALER_JSON_get_error_hint (resp_obj); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d\n", + (unsigned int) response_code, + (int) vr.hr.ec); + break; + } + auditor->config_cb (auditor->config_cb_cls, + &vr); + TALER_AUDITOR_get_config_cancel (auditor); +} + + +struct TALER_AUDITOR_GetConfigHandle * +TALER_AUDITOR_get_config (struct GNUNET_CURL_Context *ctx, + const char *url, + TALER_AUDITOR_ConfigCallback config_cb, + void *config_cb_cls) +{ + struct TALER_AUDITOR_GetConfigHandle *auditor; + CURL *eh; + + auditor = GNUNET_new (struct TALER_AUDITOR_GetConfigHandle); + auditor->config_cb = config_cb; + auditor->config_cb_cls = config_cb_cls; + auditor->ctx = ctx; + auditor->vr_url = TALER_url_join (url, + "config", + NULL); + if (NULL == auditor->vr_url) + { + GNUNET_break (0); + GNUNET_free (auditor); + return NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Requesting auditor config with URL `%s'.\n", + auditor->vr_url); + eh = TALER_AUDITOR_curl_easy_get_ (auditor->vr_url); + if (NULL == eh) + { + GNUNET_break (0); + TALER_AUDITOR_get_config_cancel (auditor); + return NULL; + } + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT, + (long) 300)); + auditor->vr = GNUNET_CURL_job_add (auditor->ctx, + eh, + &config_completed_cb, + auditor); + return auditor; +} + + +void +TALER_AUDITOR_get_config_cancel (struct TALER_AUDITOR_GetConfigHandle *auditor) +{ + if (NULL != auditor->vr) + { + GNUNET_CURL_job_cancel (auditor->vr); + auditor->vr = NULL; + } + GNUNET_free (auditor->vr_url); + GNUNET_free (auditor); +} + + +/* end of auditor_api_get_config.c */ diff --git a/src/lib/auditor_api_handle.c b/src/lib/auditor_api_handle.c deleted file mode 100644 index 1d5522141..000000000 --- a/src/lib/auditor_api_handle.c +++ /dev/null @@ -1,547 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2020 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/auditor_api_handle.c - * @brief Implementation of the "handle" component of the auditor's HTTP API - * @author Sree Harsha Totakura <sreeharsha@totakura.in> - * @author Christian Grothoff - */ -#include "platform.h" -#include <microhttpd.h> -#include <gnunet/gnunet_curl_lib.h> -#include "taler_json_lib.h" -#include "taler_auditor_service.h" -#include "taler_signatures.h" -#include "auditor_api_handle.h" -#include "auditor_api_curl_defaults.h" -#include "backoff.h" - -/** - * Which revision of the Taler auditor protocol is implemented - * by this library? Used to determine compatibility. - */ -#define TALER_PROTOCOL_CURRENT 0 - -/** - * How many revisions back are we compatible to? - */ -#define TALER_PROTOCOL_AGE 0 - - -/** - * Log error related to CURL operations. - * - * @param type log level - * @param function which function failed to run - * @param code what was the curl error code - */ -#define CURL_STRERROR(type, function, code) \ - GNUNET_log (type, "Curl function `%s' has failed at `%s:%d' with error: %s", \ - function, __FILE__, __LINE__, curl_easy_strerror (code)); - -/** - * Stages of initialization for the `struct TALER_AUDITOR_Handle` - */ -enum AuditorHandleState -{ - /** - * Just allocated. - */ - MHS_INIT = 0, - - /** - * Obtained the auditor's versioning data and version. - */ - MHS_VERSION = 1, - - /** - * Failed to initialize (fatal). - */ - MHS_FAILED = 2 -}; - - -/** - * Data for the request to get the /version of a auditor. - */ -struct VersionRequest; - - -/** - * Handle to the auditor - */ -struct TALER_AUDITOR_Handle -{ - /** - * The context of this handle - */ - struct GNUNET_CURL_Context *ctx; - - /** - * The URL of the auditor (i.e. "http://auditor.taler.net/") - */ - char *url; - - /** - * Function to call with the auditor's certification data, - * NULL if this has already been done. - */ - TALER_AUDITOR_VersionCallback version_cb; - - /** - * Closure to pass to @e version_cb. - */ - void *version_cb_cls; - - /** - * Data for the request to get the /version of a auditor, - * NULL once we are past stage #MHS_INIT. - */ - struct VersionRequest *vr; - - /** - * Task for retrying /version request. - */ - struct GNUNET_SCHEDULER_Task *retry_task; - - /** - * /version data of the auditor, only valid if - * @e handshake_complete is past stage #MHS_VERSION. - */ - char *version; - - /** - * Version information for the callback. - */ - struct TALER_AUDITOR_VersionInformation vi; - - /** - * Retry /version frequency. - */ - struct GNUNET_TIME_Relative retry_delay; - - /** - * Stage of the auditor's initialization routines. - */ - enum AuditorHandleState state; - -}; - - -/* ***************** Internal /version fetching ************* */ - -/** - * Data for the request to get the /version of a auditor. - */ -struct VersionRequest -{ - /** - * The connection to auditor this request handle will use - */ - struct TALER_AUDITOR_Handle *auditor; - - /** - * The url for this handle - */ - char *url; - - /** - * Entry for this request with the `struct GNUNET_CURL_Context`. - */ - struct GNUNET_CURL_Job *job; - -}; - - -/** - * Release memory occupied by a version request. - * Note that this does not cancel the request - * itself. - * - * @param vr request to free - */ -static void -free_version_request (struct VersionRequest *vr) -{ - GNUNET_free (vr->url); - GNUNET_free (vr); -} - - -/** - * Decode the JSON in @a resp_obj from the /version response and store the data - * in the @a key_data. - * - * @param[in] resp_obj JSON object to parse - * @param[out] auditor where to store the results we decoded - * @param[out] vc where to store version compatibility data - * @return #TALER_EC_NONE on success - */ -static enum TALER_ErrorCode -decode_version_json (const json_t *resp_obj, - struct TALER_AUDITOR_Handle *auditor, - enum TALER_AUDITOR_VersionCompatibility *vc) -{ - struct TALER_AUDITOR_VersionInformation *vi = &auditor->vi; - unsigned int age; - unsigned int revision; - unsigned int current; - const char *ver; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("version", - &ver), - GNUNET_JSON_spec_fixed_auto ("auditor_public_key", - &vi->auditor_pub), - GNUNET_JSON_spec_end () - }; - - if (JSON_OBJECT != json_typeof (resp_obj)) - { - GNUNET_break_op (0); - return TALER_EC_GENERIC_JSON_INVALID; - } - /* check the version */ - if (GNUNET_OK != - GNUNET_JSON_parse (resp_obj, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return TALER_EC_GENERIC_JSON_INVALID; - } - if (3 != sscanf (ver, - "%u:%u:%u", - ¤t, - &revision, - &age)) - { - GNUNET_break_op (0); - return TALER_EC_GENERIC_VERSION_MALFORMED; - } - auditor->version = GNUNET_strdup (ver); - vi->version = auditor->version; - *vc = TALER_AUDITOR_VC_MATCH; - if (TALER_PROTOCOL_CURRENT < current) - { - *vc |= TALER_AUDITOR_VC_NEWER; - if (TALER_PROTOCOL_CURRENT < current - age) - *vc |= TALER_AUDITOR_VC_INCOMPATIBLE; - } - if (TALER_PROTOCOL_CURRENT > current) - { - *vc |= TALER_AUDITOR_VC_OLDER; - if (TALER_PROTOCOL_CURRENT - TALER_PROTOCOL_AGE > current) - *vc |= TALER_AUDITOR_VC_INCOMPATIBLE; - } - return TALER_EC_NONE; -} - - -/** - * Initiate download of /version from the auditor. - * - * @param cls auditor where to download /version from - */ -static void -request_version (void *cls); - - -/** - * Callback used when downloading the reply to a /version request - * is complete. - * - * @param cls the `struct VersionRequest` - * @param response_code HTTP response code, 0 on error - * @param gresp_obj parsed JSON result, NULL on error, must be a `const json_t *` - */ -static void -version_completed_cb (void *cls, - long response_code, - const void *gresp_obj) -{ - const json_t *resp_obj = gresp_obj; - struct VersionRequest *vr = cls; - struct TALER_AUDITOR_Handle *auditor = vr->auditor; - enum TALER_AUDITOR_VersionCompatibility vc; - struct TALER_AUDITOR_HttpResponse hr = { - .reply = resp_obj, - .http_status = (unsigned int) response_code - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received version from URL `%s' with status %ld.\n", - vr->url, - response_code); - vc = TALER_AUDITOR_VC_PROTOCOL_ERROR; - switch (response_code) - { - case 0: - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* NOTE: this design is debatable. We MAY want to throw this error at the - client. We may then still additionally internally re-try. */ - free_version_request (vr); - auditor->vr = NULL; - GNUNET_assert (NULL == auditor->retry_task); - auditor->retry_delay = EXCHANGE_LIB_BACKOFF (auditor->retry_delay); - auditor->retry_task = GNUNET_SCHEDULER_add_delayed (auditor->retry_delay, - &request_version, - auditor); - return; - case MHD_HTTP_OK: - if (NULL == resp_obj) - { - GNUNET_break_op (0); - TALER_LOG_WARNING ("NULL body for a 200-OK /version\n"); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - } - hr.ec = decode_version_json (resp_obj, - auditor, - &vc); - if (TALER_EC_NONE != hr.ec) - { - GNUNET_break_op (0); - hr.http_status = 0; - break; - } - auditor->retry_delay = GNUNET_TIME_UNIT_ZERO; /* restart quickly */ - break; - default: - hr.ec = TALER_JSON_get_error_code (resp_obj); - hr.hint = TALER_JSON_get_error_hint (resp_obj); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d\n", - (unsigned int) response_code, - (int) hr.ec); - break; - } - if (MHD_HTTP_OK != response_code) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "/version failed for auditor %p: %u!\n", - auditor, - (unsigned int) response_code); - auditor->vr = NULL; - free_version_request (vr); - auditor->state = MHS_FAILED; - /* notify application that we failed */ - auditor->version_cb (auditor->version_cb_cls, - &hr, - NULL, - vc); - return; - } - - auditor->vr = NULL; - free_version_request (vr); - TALER_LOG_DEBUG ("Switching auditor state to 'version'\n"); - auditor->state = MHS_VERSION; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Auditor %p is now READY!\n", - auditor); - /* notify application about the key information */ - auditor->version_cb (auditor->version_cb_cls, - &hr, - &auditor->vi, - vc); -} - - -/* ********************* library internal API ********* */ - - -/** - * Get the context of a auditor. - * - * @param h the auditor handle to query - * @return ctx context to execute jobs in - */ -struct GNUNET_CURL_Context * -TALER_AUDITOR_handle_to_context_ (struct TALER_AUDITOR_Handle *h) -{ - return h->ctx; -} - - -/** - * Check if the handle is ready to process requests. - * - * @param h the auditor handle to query - * @return #GNUNET_YES if we are ready, #GNUNET_NO if not - */ -int -TALER_AUDITOR_handle_is_ready_ (struct TALER_AUDITOR_Handle *h) -{ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Checking if auditor %p (%s) is now ready: %s\n", - h, - h->url, - (MHD_VERSION == h->state) ? "yes" : "no"); - return (MHS_VERSION == h->state) ? GNUNET_YES : GNUNET_NO; -} - - -/** - * Obtain the URL to use for an API request. - * - * @param h handle for the auditor - * @param path Taler API path (i.e. "/deposit-confirmation") - * @return the full URL to use with cURL - */ -char * -TALER_AUDITOR_path_to_url_ (struct TALER_AUDITOR_Handle *h, - const char *path) -{ - GNUNET_assert ('/' == path[0]); - return TALER_url_join (h->url, - path + 1, - NULL); -} - - -/* ********************* public API ******************* */ - - -/** - * Initialise a connection to the auditor. Will connect to the - * auditor and obtain information about the auditor's master public - * key and the auditor's auditor. The respective information will - * be passed to the @a version_cb once available, and all future - * interactions with the auditor will be checked to be signed - * (where appropriate) by the respective master key. - * - * @param ctx the context - * @param url HTTP base URL for the auditor - * @param version_cb function to call with the - * auditor's version information - * @param version_cb_cls closure for @a version_cb - * @return the auditor handle; NULL upon error - */ -struct TALER_AUDITOR_Handle * -TALER_AUDITOR_connect (struct GNUNET_CURL_Context *ctx, - const char *url, - TALER_AUDITOR_VersionCallback version_cb, - void *version_cb_cls) -{ - struct TALER_AUDITOR_Handle *auditor; - - /* Disable 100 continue processing */ - GNUNET_break (GNUNET_OK == - GNUNET_CURL_append_header (ctx, - "Expect:")); - auditor = GNUNET_new (struct TALER_AUDITOR_Handle); - auditor->retry_delay = GNUNET_TIME_UNIT_SECONDS; /* start slowly */ - auditor->ctx = ctx; - auditor->url = GNUNET_strdup (url); - auditor->version_cb = version_cb; - auditor->version_cb_cls = version_cb_cls; - auditor->retry_task = GNUNET_SCHEDULER_add_now (&request_version, - auditor); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Connecting to auditor at URL `%s' (%p).\n", - url, - auditor); - return auditor; -} - - -/** - * Initiate download of /version from the auditor. - * - * @param cls auditor where to download /version from - */ -static void -request_version (void *cls) -{ - struct TALER_AUDITOR_Handle *auditor = cls; - struct VersionRequest *vr; - CURL *eh; - - auditor->retry_task = NULL; - GNUNET_assert (NULL == auditor->vr); - vr = GNUNET_new (struct VersionRequest); - vr->auditor = auditor; - vr->url = TALER_AUDITOR_path_to_url_ (auditor, - "/version"); - if (NULL == vr->url) - { - struct TALER_AUDITOR_HttpResponse hr = { - .ec = TALER_EC_GENERIC_CONFIGURATION_INVALID - }; - - auditor->version_cb (auditor->version_cb_cls, - &hr, - NULL, - TALER_AUDITOR_VC_PROTOCOL_ERROR); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Requesting auditor version with URL `%s'.\n", - vr->url); - eh = TALER_AUDITOR_curl_easy_get_ (vr->url); - if (NULL == eh) - { - GNUNET_break (0); - auditor->retry_delay = EXCHANGE_LIB_BACKOFF (auditor->retry_delay); - auditor->retry_task = GNUNET_SCHEDULER_add_delayed (auditor->retry_delay, - &request_version, - auditor); - return; - } - GNUNET_break (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_TIMEOUT, - (long) 300)); - vr->job = GNUNET_CURL_job_add (auditor->ctx, - eh, - &version_completed_cb, - vr); - auditor->vr = vr; -} - - -/** - * Disconnect from the auditor - * - * @param auditor the auditor handle - */ -void -TALER_AUDITOR_disconnect (struct TALER_AUDITOR_Handle *auditor) -{ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Disconnecting from auditor at URL `%s' (%p).\n", - auditor->url, - auditor); - if (NULL != auditor->vr) - { - GNUNET_CURL_job_cancel (auditor->vr->job); - free_version_request (auditor->vr); - auditor->vr = NULL; - } - GNUNET_free (auditor->version); - if (NULL != auditor->retry_task) - { - GNUNET_SCHEDULER_cancel (auditor->retry_task); - auditor->retry_task = NULL; - } - GNUNET_free (auditor->url); - GNUNET_free (auditor); -} - - -/* end of auditor_api_handle.c */ diff --git a/src/lib/auditor_api_handle.h b/src/lib/auditor_api_handle.h deleted file mode 100644 index 7ff5bfcdb..000000000 --- a/src/lib/auditor_api_handle.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014, 2015 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/auditor_api_handle.h - * @brief Internal interface to the handle part of the auditor's HTTP API - * @author Christian Grothoff - */ -#include "platform.h" -#include <gnunet/gnunet_curl_lib.h> -#include "taler_auditor_service.h" -#include "taler_curl_lib.h" - -/** - * Get the context of a auditor. - * - * @param h the auditor handle to query - * @return ctx context to execute jobs in - */ -struct GNUNET_CURL_Context * -TALER_AUDITOR_handle_to_context_ (struct TALER_AUDITOR_Handle *h); - - -/** - * Check if the handle is ready to process requests. - * - * @param h the auditor handle to query - * @return #GNUNET_YES if we are ready, #GNUNET_NO if not - */ -int -TALER_AUDITOR_handle_is_ready_ (struct TALER_AUDITOR_Handle *h); - - -/** - * Obtain the URL to use for an API request. - * - * @param h the auditor handle to query - * @param path Taler API path (i.e. "/deposit-confirmation") - * @return the full URL to use with cURL - */ -char * -TALER_AUDITOR_path_to_url_ (struct TALER_AUDITOR_Handle *h, - const char *path); - - -/* end of auditor_api_handle.h */ diff --git a/src/lib/exchange_api_add_aml_decision.c b/src/lib/exchange_api_add_aml_decision.c new file mode 100644 index 000000000..342e1e3dc --- /dev/null +++ b/src/lib/exchange_api_add_aml_decision.c @@ -0,0 +1,246 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_add_aml_decision.c + * @brief functions to add an AML decision by an AML officer + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" +#include "taler_signatures.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" + + +struct TALER_EXCHANGE_AddAmlDecision +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_AddAmlDecisionCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP POST /aml/$OFFICER_PUB/decision request. + * + * @param cls the `struct TALER_EXCHANGE_AddAmlDecision *` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_add_aml_decision_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_AddAmlDecision *wh = cls; + const json_t *json = response; + struct TALER_EXCHANGE_AddAmlDecisionResponse adr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + wh->job = NULL; + switch (response_code) + { + case 0: + /* no reply */ + adr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + adr.hr.hint = "server offline?"; + break; + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_FORBIDDEN: + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_CONFLICT: + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange AML decision\n", + (unsigned int) response_code, + (int) adr.hr.ec); + break; + } + if (NULL != wh->cb) + { + wh->cb (wh->cb_cls, + &adr); + wh->cb = NULL; + } + TALER_EXCHANGE_add_aml_decision_cancel (wh); +} + + +struct TALER_EXCHANGE_AddAmlDecision * +TALER_EXCHANGE_add_aml_decision ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const char *justification, + struct GNUNET_TIME_Timestamp decision_time, + const struct TALER_Amount *new_threshold, + const struct TALER_PaytoHashP *h_payto, + enum TALER_AmlDecisionState new_state, + const json_t *kyc_requirements, + const struct TALER_AmlOfficerPrivateKeyP *officer_priv, + TALER_EXCHANGE_AddAmlDecisionCallback cb, + void *cb_cls) +{ + struct TALER_AmlOfficerPublicKeyP officer_pub; + struct TALER_AmlOfficerSignatureP officer_sig; + struct TALER_EXCHANGE_AddAmlDecision *wh; + CURL *eh; + json_t *body; + + GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv, + &officer_pub.eddsa_pub); + TALER_officer_aml_decision_sign (justification, + decision_time, + new_threshold, + h_payto, + new_state, + kyc_requirements, + officer_priv, + &officer_sig); + wh = GNUNET_new (struct TALER_EXCHANGE_AddAmlDecision); + wh->cb = cb; + wh->cb_cls = cb_cls; + wh->ctx = ctx; + { + char *path; + char opus[sizeof (officer_pub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &officer_pub, + sizeof (officer_pub), + opus, + sizeof (opus)); + *end = '\0'; + GNUNET_asprintf (&path, + "aml/%s/decision", + opus); + wh->url = TALER_url_join (url, + path, + NULL); + GNUNET_free (path); + } + if (NULL == wh->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (wh); + return NULL; + } + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("justification", + justification), + GNUNET_JSON_pack_data_auto ("officer_sig", + &officer_sig), + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + GNUNET_JSON_pack_uint64 ("new_state", + (uint32_t) new_state), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("kyc_requirements", + (json_t *) kyc_requirements)), + TALER_JSON_pack_amount ("new_threshold", + new_threshold), + GNUNET_JSON_pack_timestamp ("decision_time", + decision_time)); + eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&wh->post_ctx, + eh, + body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (wh->url); + return NULL; + } + json_decref (body); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + wh->url); + wh->job = GNUNET_CURL_job_add2 (ctx, + eh, + wh->post_ctx.headers, + &handle_add_aml_decision_finished, + wh); + if (NULL == wh->job) + { + TALER_EXCHANGE_add_aml_decision_cancel (wh); + return NULL; + } + return wh; +} + + +void +TALER_EXCHANGE_add_aml_decision_cancel ( + struct TALER_EXCHANGE_AddAmlDecision *wh) +{ + if (NULL != wh->job) + { + GNUNET_CURL_job_cancel (wh->job); + wh->job = NULL; + } + TALER_curl_easy_post_finished (&wh->post_ctx); + GNUNET_free (wh->url); + GNUNET_free (wh); +} diff --git a/src/lib/exchange_api_age_withdraw.c b/src/lib/exchange_api_age_withdraw.c new file mode 100644 index 000000000..ca1a11cb8 --- /dev/null +++ b/src/lib/exchange_api_age_withdraw.c @@ -0,0 +1,1125 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_age_withdraw.c + * @brief Implementation of /reserves/$RESERVE_PUB/age-withdraw requests + * @author Özgür Kesim + */ + +#include "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_curl_lib.h" +#include "taler_error_codes.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" +#include "taler_util.h" + +/** + * A CoinCandidate is populated from a master secret + */ +struct CoinCandidate +{ + /** + * Master key material for the coin candidates. + */ + struct TALER_PlanchetMasterSecretP secret; + + /** + * The details derived form the master secrets + */ + struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details; + + /** + * Blinded hash of the coin + **/ + struct TALER_BlindedCoinHashP blinded_coin_h; + +}; + + +/** + * Closure for a call to /csr-withdraw, contains data that is needed to process + * the result. + */ +struct CSRClosure +{ + /** + * Points to the actual candidate in CoinData.coin_candidates, to continue + * to build its contents based on the results from /csr-withdraw + */ + struct CoinCandidate *candidate; + + /** + * The planchet to finally generate. Points to the corresponding candidate + * in CoindData.planchet_details + */ + struct TALER_PlanchetDetail *planchet; + + /** + * Handler to the originating call to /age-withdraw, needed to either + * cancel the running age-withdraw request (on failure of the current call + * to /csr-withdraw), or to eventually perform the protocol, once all + * csr-withdraw requests have successfully finished. + */ + struct TALER_EXCHANGE_AgeWithdrawHandle *age_withdraw_handle; + + /** + * Session nonce. + */ + union GNUNET_CRYPTO_BlindSessionNonce nonce; + + /** + * Denomination information, needed for CS coins for the + * step after /csr-withdraw + */ + const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; + + /** + * Handler for the CS R request + */ + struct TALER_EXCHANGE_CsRWithdrawHandle *csr_withdraw_handle; +}; + +/** + * Data we keep per coin in the batch. + */ +struct CoinData +{ + /** + * The denomination of the coin. Must support age restriction, i.e + * its .keys.age_mask MUST not be 0 + */ + struct TALER_EXCHANGE_DenomPublicKey denom_pub; + + /** + * The Candidates for the coin + */ + struct CoinCandidate coin_candidates[TALER_CNC_KAPPA]; + + /** + * Details of the planchet(s). + */ + struct TALER_PlanchetDetail planchet_details[TALER_CNC_KAPPA]; + + /** + * Closure for each candidate of type CS for the preflight request to + * /csr-withdraw + */ + struct CSRClosure csr_cls[TALER_CNC_KAPPA]; +}; + +/** + * A /reserves/$RESERVE_PUB/age-withdraw request-handle for calls with + * pre-blinded planchets. Returned by TALER_EXCHANGE_age_withdraw_blinded. + */ +struct TALER_EXCHANGE_AgeWithdrawBlindedHandle +{ + + /** + * 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 age mask, extracted from the denominations. + * MUST be the same for all denominations + * + */ + struct TALER_AgeMask age_mask; + + /** + * Maximum age to commit to. + */ + uint8_t max_age; + + /** + * The commitment calculated as SHA512 hash over all blinded_coin_h + */ + struct TALER_AgeWithdrawCommitmentHashP h_commitment; + + /** + * Total amount requested (value plus withdraw fee). + */ + struct TALER_Amount amount_with_fee; + + /** + * Length of the @e blinded_input Array + */ + size_t num_input; + + /** + * The blinded planchet input for the call to /age-withdraw via + * TALER_EXCHANGE_age_withdraw_blinded + */ + const struct TALER_EXCHANGE_AgeWithdrawBlindedInput *blinded_input; + + /** + * 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 age-withdraw response results. + */ + TALER_EXCHANGE_AgeWithdrawBlindedCallback callback; + + /** + * Closure for @e blinded_callback + */ + void *callback_cls; +}; + +/** + * A /reserves/$RESERVE_PUB/age-withdraw request-handle for calls from + * a wallet, i. e. when blinding data is available. + */ +struct TALER_EXCHANGE_AgeWithdrawHandle +{ + + /** + * Length of the @e coin_data Array + */ + size_t num_coins; + + /** + * The base-URL of the exchange. + */ + const char *exchange_url; + + /** + * 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 age mask, extracted from the denominations. + * MUST be the same for all denominations + * + */ + struct TALER_AgeMask age_mask; + + /** + * Maximum age to commit to. + */ + uint8_t max_age; + + /** + * Array of per-coin data + */ + struct CoinData *coin_data; + + /** + * Context for curl. + */ + struct GNUNET_CURL_Context *curl_ctx; + + struct + { + /** + * Number of /csr-withdraw requests still pending. + */ + unsigned int pending; + + /** + * CURL handle for the request job. + */ + struct GNUNET_CURL_Job *job; + } csr; + + + /** + * Function to call with age-withdraw response results. + */ + TALER_EXCHANGE_AgeWithdrawCallback callback; + + /** + * Closure for @e age_withdraw_cb + */ + void *callback_cls; + + /* The Handler for the actual call to the exchange */ + struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *procotol_handle; +}; + +/** + * We got a 200 OK response for the /reserves/$RESERVE_PUB/age-withdraw operation. + * Extract the noreveal_index and return it to the caller. + * + * @param awbh operation handle + * @param j_response reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +reserve_age_withdraw_ok ( + struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh, + const json_t *j_response) +{ + struct TALER_EXCHANGE_AgeWithdrawBlindedResponse response = { + .hr.reply = j_response, + .hr.http_status = MHD_HTTP_OK, + .details.ok.h_commitment = awbh->h_commitment + }; + struct TALER_ExchangeSignatureP exchange_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint8 ("noreveal_index", + &response.details.ok.noreveal_index), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &response.details.ok.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_age_withdraw_confirmation_verify ( + &awbh->h_commitment, + response.details.ok.noreveal_index, + &response.details.ok.exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + + } + + awbh->callback (awbh->callback_cls, + &response); + /* make sure the callback isn't called again */ + awbh->callback = NULL; + + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /reserves/$RESERVE_PUB/age-withdraw request. + * + * @param cls the `struct TALER_EXCHANGE_AgeWithdrawHandle` + * @param response_code The HTTP response code + * @param response response data + */ +static void +handle_reserve_age_withdraw_blinded_finished ( + void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh = cls; + const json_t *j_response = response; + struct TALER_EXCHANGE_AgeWithdrawBlindedResponse awbr = { + .hr.reply = j_response, + .hr.http_status = (unsigned int) response_code + }; + + awbh->job = NULL; + switch (response_code) + { + case 0: + awbr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + reserve_age_withdraw_ok (awbh, + j_response)) + { + GNUNET_break_op (0); + awbr.hr.http_status = 0; + awbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == awbh->callback); + TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); + 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 */ + awbr.hr.ec = TALER_JSON_get_error_code (j_response); + awbr.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 */ + awbr.hr.ec = TALER_JSON_get_error_code (j_response); + awbr.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. */ + awbr.hr.ec = TALER_JSON_get_error_code (j_response); + awbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_CONFLICT: + /* The age requirements might not have been met */ + awbr.hr.ec = TALER_JSON_get_error_code (j_response); + awbr.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 */ + awbr.hr.ec = TALER_JSON_get_error_code (j_response); + awbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + 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 ("requirement_row", + &ptu), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + awbr.hr.http_status = 0; + awbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + awbr.hr.ec = TALER_JSON_get_error_code (j_response); + awbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + awbr.hr.ec = TALER_JSON_get_error_code (j_response); + awbr.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) awbr.hr.ec); + break; + } + awbh->callback (awbh->callback_cls, + &awbr); + TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); +} + + +/** + * Runs the actual age-withdraw operation with the blinded planchets. + * + * @param[in,out] awbh age withdraw handler + */ +static void +perform_protocol ( + struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh) +{ +#define FAIL_IF(cond) \ + do { \ + if ((cond)) \ + { \ + GNUNET_break (! (cond)); \ + goto ERROR; \ + } \ + } while (0) + + struct GNUNET_HashContext *coins_hctx = NULL; + json_t *j_denoms = NULL; + json_t *j_array_candidates = NULL; + json_t *j_request_body = NULL; + CURL *curlh = NULL; + + GNUNET_assert (0 < awbh->num_input); + awbh->age_mask = awbh->blinded_input[0].denom_pub->key.age_mask; + + FAIL_IF (GNUNET_OK != + TALER_amount_set_zero (awbh->keys->currency, + &awbh->amount_with_fee)); + /* Accumulate total value with fees */ + for (size_t i = 0; i < awbh->num_input; i++) + { + struct TALER_Amount coin_total; + const struct TALER_EXCHANGE_DenomPublicKey *dpub = + awbh->blinded_input[i].denom_pub; + + FAIL_IF (0 > + TALER_amount_add (&coin_total, + &dpub->fees.withdraw, + &dpub->value)); + FAIL_IF (0 > + TALER_amount_add (&awbh->amount_with_fee, + &awbh->amount_with_fee, + &coin_total)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Attempting to age-withdraw from reserve %s with maximum age %d\n", + TALER_B2S (&awbh->reserve_pub), + awbh->max_age); + + coins_hctx = GNUNET_CRYPTO_hash_context_start (); + FAIL_IF (NULL == coins_hctx); + + + j_denoms = json_array (); + j_array_candidates = json_array (); + FAIL_IF ((NULL == j_denoms) || + (NULL == j_array_candidates)); + + for (size_t i = 0; i< awbh->num_input; i++) + { + /* Build the denomination array */ + { + const struct TALER_EXCHANGE_DenomPublicKey *denom_pub = + awbh->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 (awbh->age_mask.bits != denom_pub->key.age_mask.bits); + + jdenom = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto (NULL, + denom_h)); + FAIL_IF (NULL == jdenom); + FAIL_IF (0 > json_array_append_new (j_denoms, + jdenom)); + + /* Build the candidate array */ + { + json_t *j_can = json_array (); + FAIL_IF (NULL == j_can); + + for (size_t k = 0; k < TALER_CNC_KAPPA; k++) + { + struct TALER_BlindedCoinHashP bch; + const struct TALER_PlanchetDetail *planchet = + &awbh->blinded_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_can, + jc)); + + TALER_coin_ev_hash (&planchet->blinded_planchet, + &planchet->denom_pub_hash, + &bch); + + GNUNET_CRYPTO_hash_context_read (coins_hctx, + &bch, + sizeof(bch)); + } + + FAIL_IF (0 > json_array_append_new (j_array_candidates, + j_can)); + } + } + } + + /* Build the hash of the commitment */ + GNUNET_CRYPTO_hash_context_finish (coins_hctx, + &awbh->h_commitment.hash); + coins_hctx = NULL; + + /* Sign the request */ + TALER_wallet_age_withdraw_sign (&awbh->h_commitment, + &awbh->amount_with_fee, + &awbh->age_mask, + awbh->max_age, + awbh->reserve_priv, + &awbh->reserve_sig); + + /* Initiate the POST-request */ + j_request_body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("denom_hs", j_denoms), + GNUNET_JSON_pack_array_steal ("blinded_coin_evs", j_array_candidates), + GNUNET_JSON_pack_uint64 ("max_age", awbh->max_age), + GNUNET_JSON_pack_data_auto ("reserve_sig", &awbh->reserve_sig)); + FAIL_IF (NULL == j_request_body); + + curlh = TALER_EXCHANGE_curl_easy_get_ (awbh->request_url); + FAIL_IF (NULL == curlh); + FAIL_IF (GNUNET_OK != + TALER_curl_easy_post (&awbh->post_ctx, + curlh, + j_request_body)); + json_decref (j_request_body); + j_request_body = NULL; + + awbh->job = GNUNET_CURL_job_add2 ( + awbh->curl_ctx, + curlh, + awbh->post_ctx.headers, + &handle_reserve_age_withdraw_blinded_finished, + awbh); + FAIL_IF (NULL == awbh->job); + + /* No errors, return */ + return; + +ERROR: + if (NULL != j_denoms) + json_decref (j_denoms); + if (NULL != j_array_candidates) + json_decref (j_array_candidates); + if (NULL != j_request_body) + json_decref (j_request_body); + if (NULL != curlh) + curl_easy_cleanup (curlh); + if (NULL != coins_hctx) + GNUNET_CRYPTO_hash_context_abort (coins_hctx); + TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); + return; +#undef FAIL_IF +} + + +/** + * @brief Callback to copy the results from the call to TALER_age_withdraw_blinded + * to the result for the originating call from TALER_age_withdraw. + * + * @param cls struct TALER_AgeWithdrawHandle + * @param awbr The response + */ +static void +copy_results ( + void *cls, + const struct TALER_EXCHANGE_AgeWithdrawBlindedResponse *awbr) +{ + struct TALER_EXCHANGE_AgeWithdrawHandle *awh = cls; + uint8_t k = awbr->details.ok.noreveal_index; + struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details[awh->num_coins]; + struct TALER_BlindedCoinHashP blinded_coin_hs[awh->num_coins]; + struct TALER_EXCHANGE_AgeWithdrawResponse resp = { + .hr = awbr->hr, + .details = { + .ok = { + .noreveal_index = awbr->details.ok.noreveal_index, + .h_commitment = awbr->details.ok.h_commitment, + .exchange_pub = awbr->details.ok.exchange_pub, + .num_coins = awh->num_coins, + .coin_details = details, + .blinded_coin_hs = blinded_coin_hs + }, + }, + }; + + for (size_t n = 0; n< awh->num_coins; n++) + { + details[n] = awh->coin_data[n].coin_candidates[k].details; + details[n].planchet = awh->coin_data[n].planchet_details[k]; + blinded_coin_hs[n] = awh->coin_data[n].coin_candidates[k].blinded_coin_h; + } + awh->callback (awh->callback_cls, + &resp); + awh->callback = NULL; +} + + +/** + * @brief Prepares and executes TALER_EXCHANGE_age_withdraw_blinded. + * If there were CS-denominations involved, started once the all calls + * to /csr-withdraw are done. + */ +static void +call_age_withdraw_blinded ( + struct TALER_EXCHANGE_AgeWithdrawHandle *awh) +{ + struct TALER_EXCHANGE_AgeWithdrawBlindedInput blinded_input[awh->num_coins]; + + /* Prepare the blinded planchets as input */ + for (size_t n = 0; n < awh->num_coins; n++) + { + blinded_input[n].denom_pub = &awh->coin_data[n].denom_pub; + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + blinded_input[n].planchet_details[k] = + awh->coin_data[n].planchet_details[k]; + } + + awh->procotol_handle = + TALER_EXCHANGE_age_withdraw_blinded ( + awh->curl_ctx, + awh->keys, + awh->exchange_url, + awh->reserve_priv, + awh->max_age, + awh->num_coins, + blinded_input, + copy_results, + awh); +} + + +/** + * Prepares the request URL for the age-withdraw request + * + * @param awbh The handler + * @param exchange_url The base-URL to the exchange + */ +static +enum GNUNET_GenericReturnValue +prepare_url ( + struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh, + const char *exchange_url) +{ + char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; + char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &awbh->reserve_pub, + sizeof (awbh->reserve_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves/%s/age-withdraw", + pub_str); + + awbh->request_url = TALER_url_join (exchange_url, + arg_str, + NULL); + if (NULL == awbh->request_url) + { + GNUNET_break (0); + TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * @brief Function called when CSR withdraw retrieval is finished + * + * @param cls the `struct CSRClosure *` + * @param csrr replies from the /csr-withdraw request + */ +static void +csr_withdraw_done ( + void *cls, + const struct TALER_EXCHANGE_CsRWithdrawResponse *csrr) +{ + struct CSRClosure *csr = cls; + struct CoinCandidate *can; + struct TALER_PlanchetDetail *planchet; + struct TALER_EXCHANGE_AgeWithdrawHandle *awh; + + GNUNET_assert (NULL != csr); + awh = csr->age_withdraw_handle; + planchet = csr->planchet; + can = csr->candidate; + + GNUNET_assert (NULL != can); + GNUNET_assert (NULL != planchet); + GNUNET_assert (NULL != awh); + + csr->csr_withdraw_handle = NULL; + + switch (csrr->hr.http_status) + { + case MHD_HTTP_OK: + { + bool success = false; + /* Complete the initialization of the coin with CS denomination */ + + TALER_denom_ewv_copy (&can->details.alg_values, + &csrr->details.ok.alg_values); + GNUNET_assert (can->details.alg_values.blinding_inputs->cipher + == GNUNET_CRYPTO_BSA_CS); + TALER_planchet_setup_coin_priv (&can->secret, + &can->details.alg_values, + &can->details.coin_priv); + TALER_planchet_blinding_secret_create (&can->secret, + &can->details.alg_values, + &can->details.blinding_key); + /* This initializes the 2nd half of the + can->planchet_detail.blinded_planchet! */ + do { + if (GNUNET_OK != + TALER_planchet_prepare (&csr->denom_pub->key, + &can->details.alg_values, + &can->details.blinding_key, + &csr->nonce, + &can->details.coin_priv, + &can->details.h_age_commitment, + &can->details.h_coin_pub, + planchet)) + { + GNUNET_break (0); + break; + } + + TALER_coin_ev_hash (&planchet->blinded_planchet, + &planchet->denom_pub_hash, + &can->blinded_coin_h); + success = true; + } while (0); + + awh->csr.pending--; + + /* No more pending requests to /csr-withdraw, we can now perform the + * actual age-withdraw operation */ + if (0 == awh->csr.pending && success) + call_age_withdraw_blinded (awh); + return; + } + default: + break; + } + TALER_EXCHANGE_age_withdraw_cancel (awh); +} + + +/** + * @brief Prepare the coins for the call to age-withdraw and calculates + * the total amount with fees. + * + * For denomination with CS as cipher, initiates the preflight to retrieve the + * csr-parameter via /csr-withdraw. + * + * @param awh The handler to the age-withdraw + * @param num_coins The number of coins in @e coin_inputs + * @param coin_inputs The input for the individual coin(-candidates) + * @return GNUNET_OK on success, GNUNET_SYSERR on failure + */ +static +enum GNUNET_GenericReturnValue +prepare_coins ( + struct TALER_EXCHANGE_AgeWithdrawHandle *awh, + size_t num_coins, + const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[ + static num_coins]) +{ +#define FAIL_IF(cond) \ + do { \ + if ((cond)) \ + { \ + GNUNET_break (! (cond)); \ + goto ERROR; \ + } \ + } while (0) + + GNUNET_assert (0 < num_coins); + awh->age_mask = coin_inputs[0].denom_pub->key.age_mask; + + awh->coin_data = GNUNET_new_array (awh->num_coins, + struct CoinData); + + for (size_t i = 0; i < num_coins; i++) + { + struct CoinData *cd = &awh->coin_data[i]; + const struct TALER_EXCHANGE_AgeWithdrawCoinInput *input = &coin_inputs[i]; + + cd->denom_pub = *input->denom_pub; + /* The mask must be the same for all coins */ + FAIL_IF (awh->age_mask.bits != input->denom_pub->key.age_mask.bits); + TALER_denom_pub_copy (&cd->denom_pub.key, + &input->denom_pub->key); + + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + struct CoinCandidate *can = &cd->coin_candidates[k]; + struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k]; + + can->secret = input->secrets[k]; + /* Derive the age restriction from the given secret and + * the maximum age */ + TALER_age_restriction_from_secret ( + &can->secret, + &input->denom_pub->key.age_mask, + awh->max_age, + &can->details.age_commitment_proof); + + TALER_age_commitment_hash (&can->details.age_commitment_proof.commitment, + &can->details.h_age_commitment); + + switch (input->denom_pub->key.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TALER_denom_ewv_copy (&can->details.alg_values, + TALER_denom_ewv_rsa_singleton ()); + TALER_planchet_setup_coin_priv (&can->secret, + &can->details.alg_values, + &can->details.coin_priv); + TALER_planchet_blinding_secret_create (&can->secret, + &can->details.alg_values, + &can->details.blinding_key); + FAIL_IF (GNUNET_OK != + TALER_planchet_prepare (&cd->denom_pub.key, + &can->details.alg_values, + &can->details.blinding_key, + NULL, + &can->details.coin_priv, + &can->details.h_age_commitment, + &can->details.h_coin_pub, + planchet)); + TALER_coin_ev_hash (&planchet->blinded_planchet, + &planchet->denom_pub_hash, + &can->blinded_coin_h); + break; + case GNUNET_CRYPTO_BSA_CS: + { + struct CSRClosure *cls = &cd->csr_cls[k]; + /** + * Save the handler and the denomination for the callback + * after the call to csr-withdraw */ + cls->age_withdraw_handle = awh; + cls->candidate = can; + cls->planchet = planchet; + cls->denom_pub = &cd->denom_pub; + TALER_cs_withdraw_nonce_derive ( + &can->secret, + &cls->nonce.cs_nonce); + cls->csr_withdraw_handle = + TALER_EXCHANGE_csr_withdraw ( + awh->curl_ctx, + awh->exchange_url, + &cd->denom_pub, + &cls->nonce.cs_nonce, + &csr_withdraw_done, + cls); + FAIL_IF (NULL == cls->csr_withdraw_handle); + + awh->csr.pending++; + break; + } + default: + FAIL_IF (1); + } + } + } + return GNUNET_OK; + +ERROR: + TALER_EXCHANGE_age_withdraw_cancel (awh); + return GNUNET_SYSERR; +#undef FAIL_IF +}; + +struct TALER_EXCHANGE_AgeWithdrawHandle * +TALER_EXCHANGE_age_withdraw ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_coins, + const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[const static + num_coins], + uint8_t max_age, + TALER_EXCHANGE_AgeWithdrawCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_AgeWithdrawHandle *awh; + + awh = GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawHandle); + awh->exchange_url = exchange_url; + awh->keys = TALER_EXCHANGE_keys_incref (keys); + awh->curl_ctx = curl_ctx; + awh->reserve_priv = reserve_priv; + awh->callback = res_cb; + awh->callback_cls = res_cb_cls; + awh->num_coins = num_coins; + awh->max_age = max_age; + + + if (GNUNET_OK != prepare_coins (awh, + num_coins, + coin_inputs)) + { + GNUNET_free (awh); + return NULL; + } + + /* If there were no CS denominations, we can now perform the actual + * age-withdraw protocol. Otherwise, there are calls to /csr-withdraw + * in flight and once they finish, the age-withdraw-protocol will be + * called from within the csr_withdraw_done-function. + */ + if (0 == awh->csr.pending) + call_age_withdraw_blinded (awh); + + return awh; +} + + +void +TALER_EXCHANGE_age_withdraw_cancel ( + struct TALER_EXCHANGE_AgeWithdrawHandle *awh) +{ + /* Cleanup coin data */ + for (unsigned int i = 0; i<awh->num_coins; i++) + { + struct CoinData *cd = &awh->coin_data[i]; + + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k]; + struct CSRClosure *cls = &cd->csr_cls[k]; + struct CoinCandidate *can = &cd->coin_candidates[k]; + + if (NULL != cls->csr_withdraw_handle) + { + TALER_EXCHANGE_csr_withdraw_cancel (cls->csr_withdraw_handle); + cls->csr_withdraw_handle = NULL; + } + TALER_blinded_planchet_free (&planchet->blinded_planchet); + TALER_denom_ewv_free (&can->details.alg_values); + } + TALER_denom_pub_free (&cd->denom_pub.key); + } + GNUNET_free (awh->coin_data); + TALER_EXCHANGE_keys_decref (awh->keys); + TALER_EXCHANGE_age_withdraw_blinded_cancel (awh->procotol_handle); + awh->procotol_handle = NULL; + GNUNET_free (awh); +} + + +struct TALER_EXCHANGE_AgeWithdrawBlindedHandle * +TALER_EXCHANGE_age_withdraw_blinded ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + uint8_t max_age, + unsigned int num_input, + const struct TALER_EXCHANGE_AgeWithdrawBlindedInput blinded_input[static + num_input], + TALER_EXCHANGE_AgeWithdrawBlindedCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh = + GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawBlindedHandle); + + awbh->num_input = num_input; + awbh->blinded_input = blinded_input; + awbh->keys = TALER_EXCHANGE_keys_incref (keys); + awbh->curl_ctx = curl_ctx; + awbh->reserve_priv = reserve_priv; + awbh->callback = res_cb; + awbh->callback_cls = res_cb_cls; + awbh->max_age = max_age; + + GNUNET_CRYPTO_eddsa_key_get_public (&awbh->reserve_priv->eddsa_priv, + &awbh->reserve_pub.eddsa_pub); + + if (GNUNET_OK != prepare_url (awbh, + exchange_url)) + return NULL; + + perform_protocol (awbh); + return awbh; +} + + +void +TALER_EXCHANGE_age_withdraw_blinded_cancel ( + struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh) +{ + if (NULL == awbh) + return; + + if (NULL != awbh->job) + { + GNUNET_CURL_job_cancel (awbh->job); + awbh->job = NULL; + } + GNUNET_free (awbh->request_url); + TALER_EXCHANGE_keys_decref (awbh->keys); + TALER_curl_easy_post_finished (&awbh->post_ctx); + GNUNET_free (awbh); +} + + +/* exchange_api_age_withdraw.c */ 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..cade528d2 --- /dev/null +++ b/src/lib/exchange_api_age_withdraw_reveal.c @@ -0,0 +1,477 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_age_withdraw_reveal.c + * @brief Implementation of /age-withdraw/$ACH/reveal requests + * @author Özgür Kesim + */ + +#include "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 "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; + + /* The reserve's public key */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /* Number of coins */ + size_t num_coins; + + /* The @e num_coins * kappa coin secrets from the age-withdraw commitment */ + const struct TALER_EXCHANGE_AgeWithdrawCoinInput *coins_input; + + /* The url for the reveal request */ + 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, + }; + 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; + } + + { + struct TALER_BlindedDenominationSignature denom_sigs[awrh->num_coins]; + json_t *j_sig; + size_t n; + + /* Reconstruct the coins and unblind the signatures */ + json_array_foreach (j_sigs, n, j_sig) + { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_blinded_denom_sig (NULL, + &denom_sigs[n]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != GNUNET_JSON_parse (j_sig, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + } + + response.details.ok.num_sigs = awrh->num_coins; + response.details.ok.blinded_denom_sigs = denom_sigs; + awrh->callback (awrh->callback_cls, + &response); + /* Make sure the callback isn't called again */ + awrh->callback = NULL; + /* Free resources */ + for (size_t i = 0; i < awrh->num_coins; i++) + TALER_blinded_denom_sig_free (&denom_sigs[i]); + } + + 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; + /* FIXME[oec]: Only handle response-codes that are in the spec */ + switch (response_code) + { + case 0: + awr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + enum GNUNET_GenericReturnValue ret; + + ret = age_withdraw_reveal_ok (awrh, + j_response); + if (GNUNET_OK != ret) + { + 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-withdraw 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-withdraw/%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 curl_ctx The context for CURL + * @param awrh The handler + */ +static +void +perform_protocol ( + struct GNUNET_CURL_Context *curl_ctx, + 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) + + j_array_of_secrets = json_array (); + FAIL_IF (NULL == j_array_of_secrets); + + 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_data_auto ("reserve_pub", + awrh->reserve_pub), + 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 (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, + const struct TALER_ReservePublicKeyP *reserve_pub, + TALER_EXCHANGE_AgeWithdrawRevealCallback reveal_cb, + void *reveal_cb_cls) +{ + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh = + GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawRevealHandle); + awrh->noreveal_index = noreveal_index; + awrh->h_commitment = *h_commitment; + awrh->num_coins = num_coins; + awrh->coins_input = coins_input; + awrh->callback = reveal_cb; + awrh->callback_cls = reveal_cb_cls; + awrh->reserve_pub = reserve_pub; + + if (GNUNET_OK != + prepare_url (exchange_url, + awrh)) + return NULL; + + perform_protocol (curl_ctx, awrh); + + return awrh; +} + + +void +TALER_EXCHANGE_age_withdraw_reveal_cancel ( + struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh) +{ + if (NULL != awrh->job) + { + GNUNET_CURL_job_cancel (awrh->job); + awrh->job = NULL; + } + TALER_curl_easy_post_finished (&awrh->post_ctx); + + if (NULL != awrh->request_url) + GNUNET_free (awrh->request_url); + + GNUNET_free (awrh); +} + + +/* exchange_api_age_withdraw_reveal.c */ diff --git a/src/lib/exchange_api_auditor_add_denomination.c b/src/lib/exchange_api_auditor_add_denomination.c index 8952cd2fc..89de0d7f1 100644 --- a/src/lib/exchange_api_auditor_add_denomination.c +++ b/src/lib/exchange_api_auditor_add_denomination.c @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "auditor_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -78,9 +79,9 @@ handle_auditor_add_denomination_finished (void *cls, { struct TALER_EXCHANGE_AuditorAddDenominationHandle *ah = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_AuditorAddDenominationResponse adr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; ah->job = NULL; @@ -89,37 +90,37 @@ handle_auditor_add_denomination_finished (void *cls, case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_GONE: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_PRECONDITION_FAILED: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ if (NULL != json) { - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange auditor-add-denomination at URL `%s'\n", (unsigned int) response_code, - (int) hr.ec, + (int) adr.hr.ec, ah->url); } else { - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = NULL; + adr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + adr.hr.hint = NULL; GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected HTTP response code %u (no JSON returned) at URL `%s'\n", (unsigned int) response_code, @@ -130,7 +131,7 @@ handle_auditor_add_denomination_finished (void *cls, if (NULL != ah->cb) { ah->cb (ah->cb_cls, - &hr); + &adr); ah->cb = NULL; } TALER_EXCHANGE_add_auditor_denomination_cancel (ah); @@ -141,7 +142,7 @@ struct TALER_EXCHANGE_AuditorAddDenominationHandle * TALER_EXCHANGE_add_auditor_denomination ( struct GNUNET_CURL_Context *ctx, const char *url, - const struct GNUNET_HashCode *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_AuditorPublicKeyP *auditor_pub, const struct TALER_AuditorSignatureP *auditor_sig, TALER_EXCHANGE_AuditorAddDenominationCallback cb, @@ -190,26 +191,24 @@ TALER_EXCHANGE_add_auditor_denomination ( body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("auditor_sig", auditor_sig)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&ah->post_ctx, - eh, - body)) + eh = TALER_AUDITOR_curl_easy_get_ (ah->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&ah->post_ctx, + eh, + body)) ) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (ah->url); - GNUNET_free (eh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", ah->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - ah->url)); ah->job = GNUNET_CURL_job_add2 (ctx, eh, ah->post_ctx.headers, diff --git a/src/lib/exchange_api_batch_deposit.c b/src/lib/exchange_api_batch_deposit.c new file mode 100644 index 000000000..3dab64526 --- /dev/null +++ b/src/lib/exchange_api_batch_deposit.c @@ -0,0 +1,726 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> + */ +/** + * @file lib/exchange_api_batch_deposit.c + * @brief Implementation of the /batch-deposit request of the exchange's HTTP API + * @author Sree Harsha Totakura <sreeharsha@totakura.in> + * @author Christian Grothoff + */ +#include "platform.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 "taler_json_lib.h" +#include "taler_auditor_service.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" + + +/** + * 1:#AUDITOR_CHANCE is the probability that we report deposits + * to the auditor. + * + * 20==5% of going to auditor. This is possibly still too high, but set + * deliberately this high for testing + */ +#define AUDITOR_CHANCE 20 + + +/** + * Entry in list of ongoing interactions with an auditor. + */ +struct TEAH_AuditorInteractionEntry +{ + /** + * DLL entry. + */ + struct TEAH_AuditorInteractionEntry *next; + + /** + * DLL entry. + */ + struct TEAH_AuditorInteractionEntry *prev; + + /** + * URL of our auditor. For logging. + */ + const char *auditor_url; + + /** + * Interaction state. + */ + struct TALER_AUDITOR_DepositConfirmationHandle *dch; + + /** + * Batch deposit this is for. + */ + struct TALER_EXCHANGE_BatchDepositHandle *dh; +}; + + +/** + * @brief A Deposit Handle + */ +struct TALER_EXCHANGE_BatchDepositHandle +{ + + /** + * The keys of the exchange. + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * Context for our curl request(s). + */ + struct GNUNET_CURL_Context *ctx; + + /** + * The url for this request. + */ + char *url; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_BatchDepositResultCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Details about the contract. + */ + struct TALER_EXCHANGE_DepositContractDetail dcd; + + /** + * Array with details about the coins. + */ + struct TALER_EXCHANGE_CoinDepositDetail *cdds; + + /** + * Hash of the merchant's wire details. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Hash over the extensions, or all zero. + */ + struct TALER_ExtensionPolicyHashP h_policy; + + /** + * Time when this confirmation was generated / when the exchange received + * the deposit request. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Exchange signature, set for #auditor_cb. + */ + struct TALER_ExchangeSignatureP exchange_sig; + + /** + * Head of DLL of interactions with this auditor. + */ + struct TEAH_AuditorInteractionEntry *ai_head; + + /** + * Tail of DLL of interactions with this auditor. + */ + struct TEAH_AuditorInteractionEntry *ai_tail; + + /** + * Result to return to the application once @e ai_head is empty. + */ + struct TALER_EXCHANGE_BatchDepositResult dr; + + /** + * Exchange signing public key, set for #auditor_cb. + */ + struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * Total amount deposited without fees as calculated by us. + */ + struct TALER_Amount total_without_fee; + + /** + * Response object to free at the end. + */ + json_t *response; + + /** + * Chance that we will inform the auditor about the deposit + * is 1:n, where the value of this field is "n". + */ + unsigned int auditor_chance; + + /** + * Length of the @e cdds array. + */ + unsigned int num_cdds; + +}; + + +/** + * Finish batch deposit operation by calling the callback. + * + * @param[in] dh handle to finished batch deposit operation + */ +static void +finish_dh (struct TALER_EXCHANGE_BatchDepositHandle *dh) +{ + dh->cb (dh->cb_cls, + &dh->dr); + TALER_EXCHANGE_batch_deposit_cancel (dh); +} + + +/** + * Function called with the result from our call to the + * auditor's /deposit-confirmation handler. + * + * @param cls closure of type `struct TEAH_AuditorInteractionEntry *` + * @param dcr response + */ +static void +acc_confirmation_cb ( + void *cls, + const struct TALER_AUDITOR_DepositConfirmationResponse *dcr) +{ + struct TEAH_AuditorInteractionEntry *aie = cls; + struct TALER_EXCHANGE_BatchDepositHandle *dh = aie->dh; + + if (MHD_HTTP_OK != dcr->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to submit deposit confirmation to auditor `%s' with HTTP status %d (EC: %d). This is acceptable if it does not happen often.\n", + aie->auditor_url, + dcr->hr.http_status, + dcr->hr.ec); + } + GNUNET_CONTAINER_DLL_remove (dh->ai_head, + dh->ai_tail, + aie); + GNUNET_free (aie); + if (NULL == dh->ai_head) + finish_dh (dh); +} + + +/** + * Function called for each auditor to give us a chance to possibly + * launch a deposit confirmation interaction. + * + * @param cls closure + * @param auditor_url base URL of the auditor + * @param auditor_pub public key of the auditor + */ +static void +auditor_cb (void *cls, + const char *auditor_url, + const struct TALER_AuditorPublicKeyP *auditor_pub) +{ + struct TALER_EXCHANGE_BatchDepositHandle *dh = cls; + const struct TALER_EXCHANGE_SigningPublicKey *spk; + struct TEAH_AuditorInteractionEntry *aie; + const struct TALER_CoinSpendSignatureP *csigs[GNUNET_NZL ( + dh->num_cdds)]; + const struct TALER_CoinSpendPublicKeyP *cpubs[GNUNET_NZL ( + dh->num_cdds)]; + + for (unsigned int i = 0; i<dh->num_cdds; i++) + { + const struct TALER_EXCHANGE_CoinDepositDetail *cdd = &dh->cdds[i]; + + csigs[i] = &cdd->coin_sig; + cpubs[i] = &cdd->coin_pub; + } + + if (0 != + GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_WEAK, + dh->auditor_chance)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Not providing deposit confirmation to auditor\n"); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Will provide deposit confirmation to auditor `%s'\n", + TALER_B2S (auditor_pub)); + spk = TALER_EXCHANGE_get_signing_key_info (dh->keys, + &dh->exchange_pub); + if (NULL == spk) + { + GNUNET_break_op (0); + return; + } + aie = GNUNET_new (struct TEAH_AuditorInteractionEntry); + aie->dh = dh; + aie->auditor_url = auditor_url; + aie->dch = TALER_AUDITOR_deposit_confirmation ( + dh->ctx, + auditor_url, + &dh->h_wire, + &dh->h_policy, + &dh->dcd.h_contract_terms, + dh->exchange_timestamp, + dh->dcd.wire_deadline, + dh->dcd.refund_deadline, + &dh->total_without_fee, + dh->num_cdds, + cpubs, + csigs, + &dh->dcd.merchant_pub, + &dh->exchange_pub, + &dh->exchange_sig, + &dh->keys->master_pub, + spk->valid_from, + spk->valid_until, + spk->valid_legal, + &spk->master_sig, + &acc_confirmation_cb, + aie); + GNUNET_CONTAINER_DLL_insert (dh->ai_head, + dh->ai_tail, + aie); +} + + +/** + * Function called when we're done processing the + * HTTP /deposit request. + * + * @param cls the `struct TALER_EXCHANGE_BatchDepositHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_deposit_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_BatchDepositHandle *dh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_BatchDepositResult *dr = &dh->dr; + + dh->job = NULL; + dh->response = json_incref ((json_t*) j); + dr->hr.reply = dh->response; + dr->hr.http_status = (unsigned int) response_code; + switch (response_code) + { + case 0: + dr->hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &dh->exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &dh->exchange_pub), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("transaction_base_url", + &dr->details.ok.transaction_base_url), + NULL), + GNUNET_JSON_spec_timestamp ("exchange_timestamp", + &dh->exchange_timestamp), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr->hr.http_status = 0; + dr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (dh->keys, + &dh->exchange_pub)) + { + GNUNET_break_op (0); + dr->hr.http_status = 0; + dr->hr.ec = TALER_EC_EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE; + break; + } + { + const struct TALER_CoinSpendSignatureP *csigs[ + GNUNET_NZL (dh->num_cdds)]; + + for (unsigned int i = 0; i<dh->num_cdds; i++) + csigs[i] = &dh->cdds[i].coin_sig; + if (GNUNET_OK != + TALER_exchange_online_deposit_confirmation_verify ( + &dh->dcd.h_contract_terms, + &dh->h_wire, + &dh->h_policy, + dh->exchange_timestamp, + dh->dcd.wire_deadline, + dh->dcd.refund_deadline, + &dh->total_without_fee, + dh->num_cdds, + csigs, + &dh->dcd.merchant_pub, + &dh->exchange_pub, + &dh->exchange_sig)) + { + GNUNET_break_op (0); + dr->hr.http_status = 0; + dr->hr.ec = TALER_EC_EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE; + break; + } + } + TEAH_get_auditors_for_dc (dh->keys, + &auditor_cb, + dh); + } + dr->details.ok.exchange_sig = &dh->exchange_sig; + dr->details.ok.exchange_pub = &dh->exchange_pub; + dr->details.ok.deposit_timestamp = dh->exchange_timestamp; + 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 */ + dr->hr.ec = TALER_JSON_get_error_code (j); + dr->hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + dr->hr.ec = TALER_JSON_get_error_code (j); + dr->hr.hint = TALER_JSON_get_error_hint (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr->hr.ec = TALER_JSON_get_error_code (j); + dr->hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_CONFLICT: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &dr->details.conflict.coin_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr->hr.http_status = 0; + dr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + dr->hr.ec = TALER_JSON_get_error_code (j); + dr->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 */ + dr->hr.ec = TALER_JSON_get_error_code (j); + dr->hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr->hr.ec = TALER_JSON_get_error_code (j); + dr->hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr->hr.ec = TALER_JSON_get_error_code (j); + dr->hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange deposit\n", + (unsigned int) response_code, + dr->hr.ec); + GNUNET_break_op (0); + break; + } + if (NULL != dh->ai_head) + return; + finish_dh (dh); +} + + +struct TALER_EXCHANGE_BatchDepositHandle * +TALER_EXCHANGE_batch_deposit ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_EXCHANGE_DepositContractDetail *dcd, + unsigned int num_cdds, + const struct TALER_EXCHANGE_CoinDepositDetail cdds[static num_cdds], + TALER_EXCHANGE_BatchDepositResultCallback cb, + void *cb_cls, + enum TALER_ErrorCode *ec) +{ + struct TALER_EXCHANGE_BatchDepositHandle *dh; + json_t *deposit_obj; + json_t *deposits; + CURL *eh; + const struct GNUNET_HashCode *wallet_data_hashp; + + if (0 == num_cdds) + { + GNUNET_break (0); + return NULL; + } + if (GNUNET_TIME_timestamp_cmp (dcd->refund_deadline, + >, + dcd->wire_deadline)) + { + GNUNET_break_op (0); + *ec = TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE; + return NULL; + } + dh = GNUNET_new (struct TALER_EXCHANGE_BatchDepositHandle); + dh->auditor_chance = AUDITOR_CHANCE; + dh->cb = cb; + dh->cb_cls = cb_cls; + dh->cdds = GNUNET_memdup (cdds, + num_cdds * sizeof (*cdds)); + dh->num_cdds = num_cdds; + dh->dcd = *dcd; + if (NULL != dcd->policy_details) + TALER_deposit_policy_hash (dcd->policy_details, + &dh->h_policy); + TALER_merchant_wire_signature_hash (dcd->merchant_payto_uri, + &dcd->wire_salt, + &dh->h_wire); + deposits = json_array (); + GNUNET_assert (NULL != deposits); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (cdds[0].amount.currency, + &dh->total_without_fee)); + for (unsigned int i = 0; i<num_cdds; i++) + { + const struct TALER_EXCHANGE_CoinDepositDetail *cdd = &cdds[i]; + const struct TALER_EXCHANGE_DenomPublicKey *dki; + const struct TALER_AgeCommitmentHash *h_age_commitmentp; + struct TALER_Amount amount_without_fee; + + dki = TALER_EXCHANGE_get_denomination_key_by_hash (keys, + &cdd->h_denom_pub); + if (NULL == dki) + { + *ec = TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + GNUNET_break_op (0); + json_decref (deposits); + return NULL; + } + if (0 > + TALER_amount_subtract (&amount_without_fee, + &cdd->amount, + &dki->fees.deposit)) + { + *ec = TALER_EC_EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT; + GNUNET_break_op (0); + GNUNET_free (dh->cdds); + GNUNET_free (dh); + json_decref (deposits); + return NULL; + } + GNUNET_assert (0 <= + TALER_amount_add (&dh->total_without_fee, + &dh->total_without_fee, + &amount_without_fee)); + if (GNUNET_OK != + TALER_EXCHANGE_verify_deposit_signature_ (dcd, + &dh->h_policy, + &dh->h_wire, + cdd, + dki)) + { + *ec = TALER_EC_EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID; + GNUNET_break_op (0); + GNUNET_free (dh->cdds); + GNUNET_free (dh); + json_decref (deposits); + return NULL; + } + if (GNUNET_is_zero (&cdd->h_age_commitment)) + h_age_commitmentp = NULL; + else + h_age_commitmentp = &cdd->h_age_commitment; + GNUNET_assert ( + 0 == + json_array_append_new ( + deposits, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("contribution", + &cdd->amount), + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &cdd->h_denom_pub), + TALER_JSON_pack_denom_sig ("ub_sig", + &cdd->denom_sig), + GNUNET_JSON_pack_data_auto ("coin_pub", + &cdd->coin_pub), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + h_age_commitmentp)), + GNUNET_JSON_pack_data_auto ("coin_sig", + &cdd->coin_sig) + ))); + } + dh->url = TALER_url_join (url, + "batch-deposit", + NULL); + if (NULL == dh->url) + { + GNUNET_break (0); + *ec = TALER_EC_GENERIC_ALLOCATION_FAILURE; + GNUNET_free (dh->url); + GNUNET_free (dh->cdds); + GNUNET_free (dh); + json_decref (deposits); + return NULL; + } + + if (GNUNET_is_zero (&dcd->wallet_data_hash)) + wallet_data_hashp = NULL; + else + wallet_data_hashp = &dcd->wallet_data_hash; + + deposit_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("merchant_payto_uri", + dcd->merchant_payto_uri), + GNUNET_JSON_pack_data_auto ("wire_salt", + &dcd->wire_salt), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &dcd->h_contract_terms), + GNUNET_JSON_pack_array_steal ("coins", + deposits), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("wallet_data_hash", + wallet_data_hashp)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("policy_details", + (json_t *) dcd->policy_details)), + GNUNET_JSON_pack_timestamp ("timestamp", + dcd->wallet_timestamp), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &dcd->merchant_pub), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("refund_deadline", + dcd->refund_deadline)), + GNUNET_JSON_pack_timestamp ("wire_transfer_deadline", + dcd->wire_deadline)); + GNUNET_assert (NULL != deposit_obj); + eh = TALER_EXCHANGE_curl_easy_get_ (dh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&dh->post_ctx, + eh, + deposit_obj)) ) + { + *ec = TALER_EC_GENERIC_CURL_ALLOCATION_FAILURE; + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (deposit_obj); + GNUNET_free (dh->cdds); + GNUNET_free (dh->url); + GNUNET_free (dh); + return NULL; + } + json_decref (deposit_obj); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "URL for deposit: `%s'\n", + dh->url); + dh->ctx = ctx; + dh->keys = TALER_EXCHANGE_keys_incref (keys); + dh->job = GNUNET_CURL_job_add2 (ctx, + eh, + dh->post_ctx.headers, + &handle_deposit_finished, + dh); + return dh; +} + + +void +TALER_EXCHANGE_batch_deposit_force_dc ( + struct TALER_EXCHANGE_BatchDepositHandle *deposit) +{ + deposit->auditor_chance = 1; +} + + +void +TALER_EXCHANGE_batch_deposit_cancel ( + struct TALER_EXCHANGE_BatchDepositHandle *deposit) +{ + struct TEAH_AuditorInteractionEntry *aie; + + while (NULL != (aie = deposit->ai_head)) + { + GNUNET_assert (aie->dh == deposit); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Not sending deposit confirmation to auditor `%s' due to cancellation\n", + aie->auditor_url); + TALER_AUDITOR_deposit_confirmation_cancel (aie->dch); + GNUNET_CONTAINER_DLL_remove (deposit->ai_head, + deposit->ai_tail, + aie); + GNUNET_free (aie); + } + if (NULL != deposit->job) + { + GNUNET_CURL_job_cancel (deposit->job); + deposit->job = NULL; + } + TALER_EXCHANGE_keys_decref (deposit->keys); + GNUNET_free (deposit->url); + GNUNET_free (deposit->cdds); + TALER_curl_easy_post_finished (&deposit->post_ctx); + json_decref (deposit->response); + GNUNET_free (deposit); +} + + +/* end of exchange_api_batch_deposit.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..a1b21f347 --- /dev/null +++ b/src/lib/exchange_api_batch_withdraw.c @@ -0,0 +1,463 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @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 <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 "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 GNUNET_CRYPTO_BlindingSecretP bks; + + /** + * Session nonce. + */ + union GNUNET_CRYPTO_BlindSessionNonce nonce; + + /** + * 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 GNUNET_CRYPTO_BSA_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 curl context to use + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * The base URL to the exchange + */ + const char *exchange_url; + + /** + * The /keys information from the exchange + */ + const struct TALER_EXCHANGE_Keys *keys; + + /** + * 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 bw2r response data + */ +static void +handle_reserve_batch_withdraw_finished ( + void *cls, + const struct TALER_EXCHANGE_BatchWithdraw2Response *bw2r) +{ + struct TALER_EXCHANGE_BatchWithdrawHandle *wh = cls; + struct TALER_EXCHANGE_BatchWithdrawResponse wr = { + .hr = bw2r->hr + }; + struct TALER_EXCHANGE_PrivateCoinDetails coins[GNUNET_NZL (wh->num_coins)]; + + wh->wh2 = NULL; + memset (coins, + 0, + sizeof (coins)); + switch (bw2r->hr.http_status) + { + case MHD_HTTP_OK: + { + if (bw2r->details.ok.blind_sigs_length != wh->num_coins) + { + GNUNET_break_op (0); + wr.hr.http_status = 0; + wr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + for (unsigned int i = 0; i<wh->num_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, + &bw2r->details.ok.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.ok.coins = coins; + wr.details.ok.num_coins = wh->num_coins; + break; + } + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ( + "h_payto", + &wr.details.unavailable_for_legal_reasons.h_payto), + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &wr.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (bw2r->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; i<wh->num_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; i<wh->num_coins; i++) + { + struct CoinData *cd = &wh->coins[i]; + + pds[i] = cd->pd; + } + wh->wh2 = TALER_EXCHANGE_batch_withdraw2 ( + wh->curl_ctx, + wh->exchange_url, + wh->keys, + wh->reserve_priv, + wh->num_coins, + pds, + &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 (GNUNET_CRYPTO_BSA_CS == + cd->pk.key.bsign_pub_key->cipher); + switch (csrr->hr.http_status) + { + case MHD_HTTP_OK: + GNUNET_assert (NULL == + cd->alg_values.blinding_inputs); + TALER_denom_ewv_copy (&cd->alg_values, + &csrr->details.ok.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); + if (GNUNET_OK != + TALER_planchet_prepare (&cd->pk.key, + &cd->alg_values, + &cd->bks, + &cd->nonce, + &cd->priv, + cd->ach, + &cd->c_hash, + &cd->pd)) + { + GNUNET_break (0); + wr.hr.http_status = 0; + wr.hr.ec = TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR; + wh->cb (wh->cb_cls, + &wr); + TALER_EXCHANGE_batch_withdraw_cancel (wh); + return; + } + 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 GNUNET_CURL_Context *curl_ctx, + const char *exchange_url, + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ReservePrivateKeyP *reserve_priv, + unsigned int wci_length, + const struct TALER_EXCHANGE_WithdrawCoinInput wcis[static wci_length], + TALER_EXCHANGE_BatchWithdrawCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_BatchWithdrawHandle *wh; + + wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdrawHandle); + wh->curl_ctx = curl_ctx; + wh->exchange_url = exchange_url; + wh->keys = keys; + 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; i<wci_length; i++) + { + struct CoinData *cd = &wh->coins[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_copy (&cd->pk.key, + &wci->pk->key); + switch (wci->pk->key.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TALER_denom_ewv_copy (&cd->alg_values, + TALER_denom_ewv_rsa_singleton ()); + 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, + NULL, + &cd->priv, + cd->ach, + &cd->c_hash, + &cd->pd)) + { + GNUNET_break (0); + TALER_EXCHANGE_batch_withdraw_cancel (wh); + return NULL; + } + break; + case GNUNET_CRYPTO_BSA_CS: + TALER_cs_withdraw_nonce_derive ( + &cd->ps, + &cd->nonce.cs_nonce); + cd->csrh = TALER_EXCHANGE_csr_withdraw ( + curl_ctx, + exchange_url, + &cd->pk, + &cd->nonce.cs_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; i<wh->num_coins; i++) + { + struct CoinData *cd = &wh->coins[i]; + + if (NULL != cd->csrh) + { + TALER_EXCHANGE_csr_withdraw_cancel (cd->csrh); + cd->csrh = NULL; + } + TALER_denom_ewv_free (&cd->alg_values); + 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..ff1496466 --- /dev/null +++ b/src/lib/exchange_api_batch_withdraw2.c @@ -0,0 +1,441 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @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 <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 "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 url for this request. + */ + char *url; + + /** + * The /keys material from the exchange + */ + const struct TALER_EXCHANGE_Keys *keys; + + /** + * 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. As we do not have the + * blinding factor, the signature CANNOT be verified. + * + * 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[GNUNET_NZL ( + wh->num_coins)]; + const json_t *ja = json_object_get (json, + "ev_sigs"); + const json_t *j; + size_t index; + struct TALER_EXCHANGE_BatchWithdraw2Response bwr = { + .hr.reply = json, + .hr.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 (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + for (size_t i = 0; i<index; i++) + TALER_blinded_denom_sig_free (&blind_sigs[i]); + return GNUNET_SYSERR; + } + } + + /* signature is valid, return it to the application */ + bwr.details.ok.blind_sigs = blind_sigs; + bwr.details.ok.blind_sigs_length = wh->num_coins; + wh->cb (wh->cb_cls, + &bwr); + /* make sure callback isn't called again after return */ + wh->cb = NULL; + for (unsigned int i = 0; i<wh->num_coins; i++) + TALER_blinded_denom_sig_free (&blind_sigs[i]); + + 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_BatchWithdraw2Response bwr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + wh->job = NULL; + switch (response_code) + { + case 0: + bwr.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); + bwr.hr.http_status = 0; + bwr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == wh->cb); + TALER_EXCHANGE_batch_withdraw2_cancel (wh); + 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 */ + bwr.hr.ec = TALER_JSON_get_error_code (j); + bwr.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 */ + bwr.hr.ec = TALER_JSON_get_error_code (j); + bwr.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. */ + bwr.hr.ec = TALER_JSON_get_error_code (j); + bwr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + bwr.hr.ec = TALER_JSON_get_error_code (j); + bwr.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 */ + bwr.hr.ec = TALER_JSON_get_error_code (j); + bwr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ( + "h_payto", + &bwr.details.unavailable_for_legal_reasons.h_payto), + GNUNET_JSON_spec_uint64 ("requirement_row", + &bwr.details.unavailable_for_legal_reasons. + kyc_requirement_id), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + bwr.hr.http_status = 0; + bwr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + bwr.hr.ec = TALER_JSON_get_error_code (j); + bwr.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + bwr.hr.ec = TALER_JSON_get_error_code (j); + bwr.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) bwr.hr.ec); + break; + } + if (NULL != wh->cb) + { + wh->cb (wh->cb_cls, + &bwr); + wh->cb = NULL; + } + TALER_EXCHANGE_batch_withdraw2_cancel (wh); +} + + +struct TALER_EXCHANGE_BatchWithdraw2Handle * +TALER_EXCHANGE_batch_withdraw2 ( + struct GNUNET_CURL_Context *curl_ctx, + const char *exchange_url, + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ReservePrivateKeyP *reserve_priv, + unsigned int pds_length, + const struct TALER_PlanchetDetail pds[static pds_length], + TALER_EXCHANGE_BatchWithdraw2Callback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_BatchWithdraw2Handle *wh; + 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; + + GNUNET_assert (NULL != keys); + wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdraw2Handle); + wh->keys = keys; + wh->cb = res_cb; + wh->cb_cls = res_cb_cls; + wh->num_coins = pds_length; + GNUNET_assert (GNUNET_OK == + 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 = TALER_url_join (exchange_url, + arg_str, + NULL); + 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; i<pds_length; i++) + { + const struct TALER_PlanchetDetail *pd = &pds[i]; + struct TALER_Amount coin_total; + json_t *withdraw_obj; + + dk = TALER_EXCHANGE_get_denomination_key_by_hash (keys, + &pd->denom_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; + } + TALER_coin_ev_hash (&pd->blinded_planchet, + &pd->denom_pub_hash, + &bch); + 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; + json_t *req; + + req = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("planchets", + jc)); + 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 (curl_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_coins_history.c b/src/lib/exchange_api_coins_history.c new file mode 100644 index 000000000..0999e185e --- /dev/null +++ b/src/lib/exchange_api_coins_history.c @@ -0,0 +1,1230 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_coins_history.c + * @brief Implementation of the POST /coins/$COIN_PUB/history requests + * @author Christian Grothoff + * + * NOTE: this is an incomplete draft, never finished! + */ +#include "platform.h" +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP history codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 /coins/$RID/history Handle + */ +struct TALER_EXCHANGE_CoinsHistoryHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_CoinsHistoryCallback cb; + + /** + * Public key of the coin we are querying. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Closure for @a cb. + */ + void *cb_cls; + +}; + + +/** + * Context for coin helpers. + */ +struct CoinHistoryParseContext +{ + + /** + * Keys of the exchange. + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * Denomination of the coin. + */ + const struct TALER_EXCHANGE_DenomPublicKey *dk; + + /** + * Our coin public key. + */ + const struct TALER_CoinSpendPublicKeyP *coin_pub; + + /** + * Where to sum up total refunds. + */ + struct TALER_Amount *total_in; + + /** + * Total amount encountered. + */ + struct TALER_Amount *total_out; + +}; + + +/** + * Signature of functions that operate on one of + * the coin's history entries. + * + * @param[in,out] pc overall context + * @param[out] rh where to write the history entry + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +typedef enum GNUNET_GenericReturnValue +(*CoinCheckHelper)(struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction); + + +/** + * Handle deposit entry in the coin's history. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_deposit (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &rh->details.deposit.sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &rh->details.deposit.h_contract_terms), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("wallet_data_hash", + &rh->details.deposit.wallet_data_hash), + &rh->details.deposit.no_wallet_data_hash), + GNUNET_JSON_spec_fixed_auto ("h_wire", + &rh->details.deposit.h_wire), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &rh->details.deposit.hac), + &rh->details.deposit.no_hac), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_policy", + &rh->details.deposit.h_policy), + &rh->details.deposit.no_h_policy), + GNUNET_JSON_spec_timestamp ("timestamp", + &rh->details.deposit.wallet_timestamp), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("refund_deadline", + &rh->details.deposit.refund_deadline), + NULL), + TALER_JSON_spec_amount_any ("deposit_fee", + &rh->details.deposit.deposit_fee), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &rh->details.deposit.merchant_pub), + GNUNET_JSON_spec_end () + }; + + rh->details.deposit.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_deposit_verify ( + amount, + &rh->details.deposit.deposit_fee, + &rh->details.deposit.h_wire, + &rh->details.deposit.h_contract_terms, + rh->details.deposit.no_wallet_data_hash + ? NULL + : &rh->details.deposit.wallet_data_hash, + rh->details.deposit.no_hac + ? NULL + : &rh->details.deposit.hac, + rh->details.deposit.no_h_policy + ? NULL + : &rh->details.deposit.h_policy, + &pc->dk->h_key, + rh->details.deposit.wallet_timestamp, + &rh->details.deposit.merchant_pub, + rh->details.deposit.refund_deadline, + pc->coin_pub, + &rh->details.deposit.sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + /* check that deposit fee matches our expectations from /keys! */ + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&rh->details.deposit.deposit_fee, + &pc->dk->fees.deposit)) || + (0 != + TALER_amount_cmp (&rh->details.deposit.deposit_fee, + &pc->dk->fees.deposit)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_YES; +} + + +/** + * Handle melt entry in the coin's history. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_melt (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &rh->details.melt.sig), + GNUNET_JSON_spec_fixed_auto ("rc", + &rh->details.melt.rc), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &rh->details.melt.h_age_commitment), + &rh->details.melt.no_hac), + TALER_JSON_spec_amount_any ("melt_fee", + &rh->details.melt.melt_fee), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + /* check that melt fee matches our expectations from /keys! */ + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&rh->details.melt.melt_fee, + &pc->dk->fees.refresh)) || + (0 != + TALER_amount_cmp (&rh->details.melt.melt_fee, + &pc->dk->fees.refresh)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_melt_verify ( + amount, + &rh->details.melt.melt_fee, + &rh->details.melt.rc, + &pc->dk->h_key, + rh->details.melt.no_hac + ? NULL + : &rh->details.melt.h_age_commitment, + pc->coin_pub, + &rh->details.melt.sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_YES; +} + + +/** + * Handle refund entry in the coin's history. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_refund (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("refund_fee", + &rh->details.refund.refund_fee), + GNUNET_JSON_spec_fixed_auto ("merchant_sig", + &rh->details.refund.sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &rh->details.refund.h_contract_terms), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &rh->details.refund.merchant_pub), + GNUNET_JSON_spec_uint64 ("rtransaction_id", + &rh->details.refund.rtransaction_id), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (&rh->details.refund.sig_amount, + &rh->details.refund.refund_fee, + amount)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_merchant_refund_verify (pc->coin_pub, + &rh->details.refund.h_contract_terms, + rh->details.refund.rtransaction_id, + &rh->details.refund.sig_amount, + &rh->details.refund.merchant_pub, + &rh->details.refund.sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + /* NOTE: theoretically, we could also check that the given + merchant_pub and h_contract_terms appear in the + history under deposits. However, there is really no benefit + for the exchange to lie here, so not checking is probably OK + (an auditor ought to check, though). Then again, we similarly + had no reason to check the merchant's signature (other than a + well-formendess check). */ + + /* check that refund fee matches our expectations from /keys! */ + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&rh->details.refund.refund_fee, + &pc->dk->fees.refund)) || + (0 != + TALER_amount_cmp (&rh->details.refund.refund_fee, + &pc->dk->fees.refund)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_NO; +} + + +/** + * Handle recoup entry in the coin's history. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_recoup (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &rh->details.recoup.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &rh->details.recoup.exchange_pub), + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &rh->details.recoup.reserve_pub), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &rh->details.recoup.coin_sig), + GNUNET_JSON_spec_fixed_auto ("coin_blind", + &rh->details.recoup.coin_bks), + GNUNET_JSON_spec_timestamp ("timestamp", + &rh->details.recoup.timestamp), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_exchange_online_confirm_recoup_verify ( + rh->details.recoup.timestamp, + amount, + pc->coin_pub, + &rh->details.recoup.reserve_pub, + &rh->details.recoup.exchange_pub, + &rh->details.recoup.exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_recoup_verify (&pc->dk->h_key, + &rh->details.recoup.coin_bks, + pc->coin_pub, + &rh->details.recoup.coin_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_YES; +} + + +/** + * Handle recoup-refresh entry in the coin's history. + * This is the coin that was subjected to a recoup, + * the value being credited to the old coin. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_recoup_refresh (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &rh->details.recoup_refresh.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &rh->details.recoup_refresh.exchange_pub), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &rh->details.recoup_refresh.coin_sig), + GNUNET_JSON_spec_fixed_auto ("old_coin_pub", + &rh->details.recoup_refresh.old_coin_pub), + GNUNET_JSON_spec_fixed_auto ("coin_blind", + &rh->details.recoup_refresh.coin_bks), + GNUNET_JSON_spec_timestamp ("timestamp", + &rh->details.recoup_refresh.timestamp), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_exchange_online_confirm_recoup_refresh_verify ( + rh->details.recoup_refresh.timestamp, + amount, + pc->coin_pub, + &rh->details.recoup_refresh.old_coin_pub, + &rh->details.recoup_refresh.exchange_pub, + &rh->details.recoup_refresh.exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_recoup_verify (&pc->dk->h_key, + &rh->details.recoup_refresh.coin_bks, + pc->coin_pub, + &rh->details.recoup_refresh.coin_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_YES; +} + + +/** + * Handle old coin recoup entry in the coin's history. + * This is the coin that was credited in a recoup, + * the value being credited to the this coin. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_old_coin_recoup (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &rh->details.old_coin_recoup.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &rh->details.old_coin_recoup.exchange_pub), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &rh->details.old_coin_recoup.new_coin_pub), + GNUNET_JSON_spec_timestamp ("timestamp", + &rh->details.old_coin_recoup.timestamp), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_exchange_online_confirm_recoup_refresh_verify ( + rh->details.old_coin_recoup.timestamp, + amount, + &rh->details.old_coin_recoup.new_coin_pub, + pc->coin_pub, + &rh->details.old_coin_recoup.exchange_pub, + &rh->details.old_coin_recoup.exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_NO; +} + + +/** + * Handle purse deposit entry in the coin's history. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_purse_deposit (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("purse_pub", + &rh->details.purse_deposit.purse_pub), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &rh->details.purse_deposit.coin_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &rh->details.purse_deposit.phac), + NULL), + TALER_JSON_spec_web_url ("exchange_base_url", + &rh->details.purse_deposit.exchange_base_url), + GNUNET_JSON_spec_bool ("refunded", + &rh->details.purse_deposit.refunded), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_purse_deposit_verify ( + rh->details.purse_deposit.exchange_base_url, + &rh->details.purse_deposit.purse_pub, + amount, + &pc->dk->h_key, + &rh->details.purse_deposit.phac, + pc->coin_pub, + &rh->details.purse_deposit.coin_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (rh->details.purse_deposit.refunded) + { + /* We wave the deposit fee. */ + if (0 > + TALER_amount_add (pc->total_in, + pc->total_in, + &pc->dk->fees.deposit)) + { + /* overflow in refund history? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + return GNUNET_YES; +} + + +/** + * Handle purse refund entry in the coin's history. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_purse_refund (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("refund_fee", + &rh->details.purse_refund.refund_fee), + GNUNET_JSON_spec_fixed_auto ("purse_pub", + &rh->details.purse_refund.purse_pub), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &rh->details.purse_refund.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &rh->details.purse_refund.exchange_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_exchange_online_purse_refund_verify ( + amount, + &rh->details.purse_refund.refund_fee, + pc->coin_pub, + &rh->details.purse_refund.purse_pub, + &rh->details.purse_refund.exchange_pub, + &rh->details.purse_refund.exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&rh->details.purse_refund.refund_fee, + &pc->dk->fees.refund)) || + (0 != + TALER_amount_cmp (&rh->details.purse_refund.refund_fee, + &pc->dk->fees.refund)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_NO; +} + + +/** + * Handle reserve deposit entry in the coin's history. + * + * @param[in,out] pc overall context + * @param[out] rh history entry to initialize + * @param amount main amount of this operation + * @param transaction JSON details for the operation + * @return #GNUNET_SYSERR on error, + * #GNUNET_OK to add, #GNUNET_NO to subtract + */ +static enum GNUNET_GenericReturnValue +help_reserve_open_deposit (struct CoinHistoryParseContext *pc, + struct TALER_EXCHANGE_CoinHistoryEntry *rh, + const struct TALER_Amount *amount, + json_t *transaction) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rh->details.reserve_open_deposit.reserve_sig), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &rh->details.reserve_open_deposit.coin_sig), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_reserve_open_deposit_verify ( + amount, + &rh->details.reserve_open_deposit.reserve_sig, + pc->coin_pub, + &rh->details.reserve_open_deposit.coin_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_YES; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_parse_coin_history ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_EXCHANGE_DenomPublicKey *dk, + const json_t *history, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + struct TALER_Amount *total_in, + struct TALER_Amount *total_out, + unsigned int rlen, + struct TALER_EXCHANGE_CoinHistoryEntry rhistory[static rlen]) +{ + const struct + { + const char *type; + CoinCheckHelper helper; + enum TALER_EXCHANGE_CoinTransactionType ctt; + } map[] = { + { "DEPOSIT", + &help_deposit, + TALER_EXCHANGE_CTT_DEPOSIT }, + { "MELT", + &help_melt, + TALER_EXCHANGE_CTT_MELT }, + { "REFUND", + &help_refund, + TALER_EXCHANGE_CTT_REFUND }, + { "RECOUP", + &help_recoup, + TALER_EXCHANGE_CTT_RECOUP }, + { "RECOUP-REFRESH", + &help_recoup_refresh, + TALER_EXCHANGE_CTT_RECOUP_REFRESH }, + { "OLD-COIN-RECOUP", + &help_old_coin_recoup, + TALER_EXCHANGE_CTT_OLD_COIN_RECOUP }, + { "PURSE-DEPOSIT", + &help_purse_deposit, + TALER_EXCHANGE_CTT_PURSE_DEPOSIT }, + { "PURSE-REFUND", + &help_purse_refund, + TALER_EXCHANGE_CTT_PURSE_REFUND }, + { "RESERVE-OPEN-DEPOSIT", + &help_reserve_open_deposit, + TALER_EXCHANGE_CTT_RESERVE_OPEN_DEPOSIT }, + { NULL, NULL, TALER_EXCHANGE_CTT_NONE } + }; + struct CoinHistoryParseContext pc = { + .dk = dk, + .coin_pub = coin_pub, + .total_out = total_out, + .total_in = total_in + }; + size_t len; + + if (NULL == history) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + len = json_array_size (history); + if (0 == len) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + *total_in = dk->value; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (total_in->currency, + total_out)); + for (size_t off = 0; off<len; off++) + { + struct TALER_EXCHANGE_CoinHistoryEntry *rh = &rhistory[off]; + json_t *transaction = json_array_get (history, + off); + enum GNUNET_GenericReturnValue add; + const char *type; + struct GNUNET_JSON_Specification spec_glob[] = { + TALER_JSON_spec_amount_any ("amount", + &rh->amount), + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + spec_glob, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_YES != + TALER_amount_cmp_currency (&rh->amount, + total_in)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Operation of type %s with amount %s\n", + type, + TALER_amount2s (&rh->amount)); + add = GNUNET_SYSERR; + for (unsigned int i = 0; NULL != map[i].type; i++) + { + if (0 == strcasecmp (type, + map[i].type)) + { + rh->type = map[i].ctt; + add = map[i].helper (&pc, + rh, + &rh->amount, + transaction); + break; + } + } + switch (add) + { + case GNUNET_SYSERR: + /* entry type not supported, new version on server? */ + rh->type = TALER_EXCHANGE_CTT_NONE; + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected type `%s' in response\n", + type); + return GNUNET_SYSERR; + case GNUNET_YES: + /* This amount should be debited from the coin */ + if (0 > + TALER_amount_add (total_out, + total_out, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + break; + case GNUNET_NO: + /* This amount should be credited to the coin. */ + if (0 > + TALER_amount_add (total_in, + total_in, + &rh->amount)) + { + /* overflow in refund history? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + break; + } /* end of switch(add) */ + } + return GNUNET_OK; +} + + +/** + * We received an #MHD_HTTP_OK history code. Handle the JSON + * response. + * + * @param rsh handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_coins_history_ok (struct TALER_EXCHANGE_CoinsHistoryHandle *rsh, + const json_t *j) +{ + struct TALER_EXCHANGE_CoinHistory rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_OK + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("balance", + &rs.details.ok.balance), + GNUNET_JSON_spec_fixed_auto ("h_denom_pub", + &rs.details.ok.h_denom_pub), + GNUNET_JSON_spec_array_const ("history", + &rs.details.ok.history), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (NULL != rsh->cb) + { + rsh->cb (rsh->cb_cls, + &rs); + rsh->cb = NULL; + } + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /coins/$RID/history request. + * + * @param cls the `struct TALER_EXCHANGE_CoinsHistoryHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_coins_history_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_CoinsHistoryHandle *rsh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_CoinHistory rs = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + rsh->job = NULL; + switch (response_code) + { + case 0: + rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + handle_coins_history_ok (rsh, + j)) + { + rs.hr.http_status = 0; + rs.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 */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.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 */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for coins history\n", + (unsigned int) response_code, + (int) rs.hr.ec); + break; + } + if (NULL != rsh->cb) + { + rsh->cb (rsh->cb_cls, + &rs); + rsh->cb = NULL; + } + TALER_EXCHANGE_coins_history_cancel (rsh); +} + + +struct TALER_EXCHANGE_CoinsHistoryHandle * +TALER_EXCHANGE_coins_history ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_CoinSpendPrivateKeyP *coin_priv, + uint64_t start_off, + TALER_EXCHANGE_CoinsHistoryCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_CoinsHistoryHandle *rsh; + CURL *eh; + char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 64]; + struct curl_slist *job_headers; + + rsh = GNUNET_new (struct TALER_EXCHANGE_CoinsHistoryHandle); + rsh->cb = cb; + rsh->cb_cls = cb_cls; + GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv, + &rsh->coin_pub.eddsa_pub); + { + char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &rsh->coin_pub, + sizeof (rsh->coin_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + if (0 != start_off) + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "coins/%s/history?start=%llu", + pub_str, + (unsigned long long) start_off); + else + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "coins/%s/history", + pub_str); + } + rsh->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == rsh->url) + { + GNUNET_free (rsh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (rsh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (rsh->url); + GNUNET_free (rsh); + return NULL; + } + + { + struct TALER_CoinSpendSignatureP coin_sig; + char *sig_hdr; + char *hdr; + + TALER_wallet_coin_history_sign (start_off, + coin_priv, + &coin_sig); + + sig_hdr = GNUNET_STRINGS_data_to_string_alloc ( + &coin_sig, + sizeof (coin_sig)); + GNUNET_asprintf (&hdr, + "%s: %s", + TALER_COIN_HISTORY_SIGNATURE_HEADER, + sig_hdr); + GNUNET_free (sig_hdr); + job_headers = curl_slist_append (NULL, + hdr); + GNUNET_free (hdr); + if (NULL == job_headers) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + return NULL; + } + } + + rsh->job = GNUNET_CURL_job_add2 (ctx, + eh, + job_headers, + &handle_coins_history_finished, + rsh); + curl_slist_free_all (job_headers); + return rsh; +} + + +void +TALER_EXCHANGE_coins_history_cancel ( + struct TALER_EXCHANGE_CoinsHistoryHandle *rsh) +{ + if (NULL != rsh->job) + { + GNUNET_CURL_job_cancel (rsh->job); + rsh->job = NULL; + } + TALER_curl_easy_post_finished (&rsh->post_ctx); + GNUNET_free (rsh->url); + GNUNET_free (rsh); +} + + +/** + * Verify that @a coin_sig does NOT appear in the @a history of a coin's + * transactions and thus whatever transaction is authorized by @a coin_sig is + * a conflict with @a proof. + * + * @param history coin history to check + * @param coin_sig signature that must not be in @a history + * @return #GNUNET_OK if @a coin_sig is not in @a history + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_signature_conflict ( + const json_t *history, + const struct TALER_CoinSpendSignatureP *coin_sig) +{ + size_t off; + json_t *entry; + + json_array_foreach (history, off, entry) + { + struct TALER_CoinSpendSignatureP cs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &cs), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (entry, + spec, + NULL, NULL)) + continue; /* entry without coin signature */ + if (0 == + GNUNET_memcmp (&cs, + coin_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +#if FIXME_IMPLEMENT +/** + * FIXME-Oec: we need some specific routines that show + * that certain coin operations are indeed in conflict, + * for example that the coin is of a different denomination + * or different age restrictions. + * This relates to unimplemented error handling for + * coins in the exchange! + * + * Check that the provided @a proof indeeds indicates + * a conflict for @a coin_pub. + * + * @param keys exchange keys + * @param proof provided conflict proof + * @param dk denomination of @a coin_pub that the client + * used + * @param coin_pub public key of the coin + * @param required balance required on the coin for the operation + * @return #GNUNET_OK if @a proof holds + */ +// FIXME: should be properly defined and implemented! +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + const json_t *proof, + const struct TALER_EXCHANGE_DenomPublicKey *dk, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *required) +{ + enum TALER_ErrorCode ec; + + ec = TALER_JSON_get_error_code (proof); + switch (ec) + { + case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: + /* Nothing to check anymore here, proof needs to be + checked in the GET /coins/$COIN_PUB handler */ + break; + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + // FIXME: write check! + break; + default: + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +#endif + + +/* end of exchange_api_coins_history.c */ diff --git a/src/lib/exchange_api_common.c b/src/lib/exchange_api_common.c index da1400b9d..bd731ad37 100644 --- a/src/lib/exchange_api_common.c +++ b/src/lib/exchange_api_common.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2020 Taler Systems SA + Copyright (C) 2015-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 @@ -22,381 +22,252 @@ #include "platform.h" #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> +#include "exchange_api_common.h" #include "exchange_api_handle.h" #include "taler_signatures.h" -/** - * Parse history given in JSON format and return it in binary - * format. - * - * @param exchange connection to the exchange we can use - * @param history JSON array with the history - * @param reserve_pub public key of the reserve to inspect - * @param currency currency we expect the balance to be in - * @param[out] balance final balance - * @param history_length number of entries in @a history - * @param[out] rhistory array of length @a history_length, set to the - * parsed history entries - * @return #GNUNET_OK if history was valid and @a rhistory and @a balance - * were set, - * #GNUNET_SYSERR if there was a protocol violation in @a history - */ -int -TALER_EXCHANGE_parse_reserve_history ( - struct TALER_EXCHANGE_Handle *exchange, - const json_t *history, - const struct TALER_ReservePublicKeyP *reserve_pub, - const char *currency, - struct TALER_Amount *balance, - unsigned int history_length, - struct TALER_EXCHANGE_ReserveHistory *rhistory) +const struct TALER_EXCHANGE_SigningPublicKey * +TALER_EXCHANGE_get_signing_key_info ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ExchangePublicKeyP *exchange_pub) { - struct GNUNET_HashCode uuid[history_length]; - unsigned int uuid_off; - struct TALER_Amount total_in; - struct TALER_Amount total_out; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (currency, - &total_in)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (currency, - &total_out)); - uuid_off = 0; - for (unsigned int off = 0; off<history_length; off++) + for (unsigned int i = 0; i<keys->num_sign_keys; i++) { - struct TALER_EXCHANGE_ReserveHistory *rh = &rhistory[off]; - json_t *transaction; - struct TALER_Amount amount; - const char *type; - struct GNUNET_JSON_Specification hist_spec[] = { - GNUNET_JSON_spec_string ("type", - &type), - TALER_JSON_spec_amount_any ("amount", - &amount), - /* 'wire' and 'signature' are optional depending on 'type'! */ - GNUNET_JSON_spec_end () - }; + const struct TALER_EXCHANGE_SigningPublicKey *spk + = &keys->sign_keys[i]; - transaction = json_array_get (history, - off); - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - hist_spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - rhistory[off].amount = amount; - if (GNUNET_YES != - TALER_amount_cmp_currency (&amount, - &total_in)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 == strcasecmp (type, - "CREDIT")) - { - const char *wire_url; - uint64_t wire_reference; - struct GNUNET_TIME_Absolute timestamp; - struct GNUNET_JSON_Specification withdraw_spec[] = { - GNUNET_JSON_spec_uint64 ("wire_reference", - &wire_reference), - TALER_JSON_spec_absolute_time ("timestamp", - ×tamp), - GNUNET_JSON_spec_string ("sender_account_url", - &wire_url), - GNUNET_JSON_spec_end () - }; + if (0 == GNUNET_memcmp (exchange_pub, + &spk->key)) + return spk; + } + return NULL; +} - rh->type = TALER_EXCHANGE_RTT_CREDIT; - if (0 > - TALER_amount_add (&total_in, - &total_in, - &amount)) - { - /* overflow in history already!? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - withdraw_spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - rh->details.in_details.sender_url = GNUNET_strdup (wire_url); - rh->details.in_details.wire_reference = wire_reference; - rh->details.in_details.timestamp = timestamp; - /* end type==DEPOSIT */ - } - else if (0 == strcasecmp (type, - "WITHDRAW")) - { - struct TALER_ReserveSignatureP sig; - struct TALER_WithdrawRequestPS withdraw_purpose; - struct TALER_Amount withdraw_fee; - struct GNUNET_JSON_Specification withdraw_spec[] = { - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &sig), - TALER_JSON_spec_amount_any ("withdraw_fee", - &withdraw_fee), - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &withdraw_purpose.h_denomination_pub), - GNUNET_JSON_spec_fixed_auto ("h_coin_envelope", - &withdraw_purpose.h_coin_envelope), - GNUNET_JSON_spec_end () - }; - rh->type = TALER_EXCHANGE_RTT_WITHDRAWAL; - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - withdraw_spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - withdraw_purpose.purpose.size - = htonl (sizeof (withdraw_purpose)); - withdraw_purpose.purpose.purpose - = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW); - withdraw_purpose.reserve_pub = *reserve_pub; - TALER_amount_hton (&withdraw_purpose.amount_with_fee, - &amount); - /* Check that the signature is a valid withdraw request */ - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW, - &withdraw_purpose, - &sig.eddsa_signature, - &reserve_pub->eddsa_pub)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (withdraw_spec); - return GNUNET_SYSERR; - } - /* check that withdraw fee matches expectations! */ - { - const struct TALER_EXCHANGE_Keys *key_state; - const struct TALER_EXCHANGE_DenomPublicKey *dki; - - key_state = TALER_EXCHANGE_get_keys (exchange); - dki = TALER_EXCHANGE_get_denomination_key_by_hash (key_state, - &withdraw_purpose. - h_denomination_pub); - if ( (GNUNET_YES != - TALER_amount_cmp_currency (&withdraw_fee, - &dki->fee_withdraw)) || - (0 != - TALER_amount_cmp (&withdraw_fee, - &dki->fee_withdraw)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (withdraw_spec); - return GNUNET_SYSERR; - } - rh->details.withdraw.fee = withdraw_fee; - } - rh->details.withdraw.out_authorization_sig - = json_object_get (transaction, - "signature"); - /* Check check that the same withdraw transaction - isn't listed twice by the exchange. We use the - "uuid" array to remember the hashes of all - purposes, and compare the hashes to find - duplicates. */// - GNUNET_CRYPTO_hash (&withdraw_purpose, - ntohl (withdraw_purpose.purpose.size), - &uuid[uuid_off]); - for (unsigned int i = 0; i<uuid_off; i++) - { - if (0 == GNUNET_memcmp (&uuid[uuid_off], - &uuid[i])) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (withdraw_spec); - return GNUNET_SYSERR; - } - } - uuid_off++; +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_create_conflict_ ( + const struct TALER_PurseContractSignatureP *cpurse_sig, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *proof) +{ + struct TALER_Amount amount; + uint32_t min_age; + struct GNUNET_TIME_Timestamp purse_expiration; + struct TALER_PurseContractSignatureP purse_sig; + struct TALER_PrivateContractHashP h_contract_terms; + struct TALER_PurseMergePublicKeyP merge_pub; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("amount", + &amount), + GNUNET_JSON_spec_uint32 ("min_age", + &min_age), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &purse_expiration), + GNUNET_JSON_spec_fixed_auto ("purse_sig", + &purse_sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &h_contract_terms), + GNUNET_JSON_spec_fixed_auto ("merge_pub", + &merge_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (proof, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_purse_create_verify (purse_expiration, + &h_contract_terms, + &merge_pub, + min_age, + &amount, + purse_pub, + &purse_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == + GNUNET_memcmp (&purse_sig, + cpurse_sig)) + { + /* Must be the SAME data, not a conflict! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} - if (0 > - TALER_amount_add (&total_out, - &total_out, - &amount)) - { - /* overflow in history already!? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - GNUNET_JSON_parse_free (withdraw_spec); - return GNUNET_SYSERR; - } - /* end type==WITHDRAW */ - } - else if (0 == strcasecmp (type, - "RECOUP")) - { - struct TALER_RecoupConfirmationPS pc; - struct GNUNET_TIME_Absolute timestamp; - const struct TALER_EXCHANGE_Keys *key_state; - struct GNUNET_JSON_Specification recoup_spec[] = { - GNUNET_JSON_spec_fixed_auto ("coin_pub", - &pc.coin_pub), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &rh->details.recoup_details.exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &rh->details.recoup_details.exchange_pub), - TALER_JSON_spec_absolute_time_nbo ("timestamp", - &pc.timestamp), - GNUNET_JSON_spec_end () - }; - rh->type = TALER_EXCHANGE_RTT_RECOUP; - rh->amount = amount; - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - recoup_spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - rh->details.recoup_details.coin_pub = pc.coin_pub; - TALER_amount_hton (&pc.recoup_amount, - &amount); - pc.purpose.size = htonl (sizeof (pc)); - pc.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP); - pc.reserve_pub = *reserve_pub; - timestamp = GNUNET_TIME_absolute_ntoh (pc.timestamp); - rh->details.recoup_details.timestamp = timestamp; - - key_state = TALER_EXCHANGE_get_keys (exchange); - if (GNUNET_OK != - TALER_EXCHANGE_test_signing_key (key_state, - &rh->details. - recoup_details.exchange_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP, - &pc, - &rh->details.recoup_details.exchange_sig.eddsa_signature, - &rh->details.recoup_details.exchange_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (&total_in, - &total_in, - &rh->amount)) - { - /* overflow in history already!? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - /* end type==RECOUP */ - } - else if (0 == strcasecmp (type, - "CLOSING")) - { - const struct TALER_EXCHANGE_Keys *key_state; - struct TALER_ReserveCloseConfirmationPS rcc; - struct GNUNET_TIME_Absolute timestamp; - struct GNUNET_JSON_Specification closing_spec[] = { - GNUNET_JSON_spec_string ( - "receiver_account_details", - &rh->details.close_details.receiver_account_details), - GNUNET_JSON_spec_fixed_auto ("wtid", - &rh->details.close_details.wtid), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &rh->details.close_details.exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &rh->details.close_details.exchange_pub), - TALER_JSON_spec_amount_any_nbo ("closing_fee", - &rcc.closing_fee), - TALER_JSON_spec_absolute_time_nbo ("timestamp", - &rcc.timestamp), - GNUNET_JSON_spec_end () - }; +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_merge_conflict_ ( + const struct TALER_PurseMergeSignatureP *cmerge_sig, + const struct TALER_PurseMergePublicKeyP *merge_pub, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const char *exchange_url, + const json_t *proof) +{ + struct TALER_PurseMergeSignatureP merge_sig; + struct GNUNET_TIME_Timestamp merge_timestamp; + const char *partner_url = NULL; + struct TALER_ReservePublicKeyP reserve_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("partner_url", + &partner_url), + NULL), + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &merge_timestamp), + GNUNET_JSON_spec_fixed_auto ("merge_sig", + &merge_sig), + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &reserve_pub), + GNUNET_JSON_spec_end () + }; + char *payto_uri; + + if (GNUNET_OK != + GNUNET_JSON_parse (proof, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (NULL == partner_url) + partner_url = exchange_url; + payto_uri = TALER_reserve_make_payto (partner_url, + &reserve_pub); + if (GNUNET_OK != + TALER_wallet_purse_merge_verify ( + payto_uri, + merge_timestamp, + purse_pub, + merge_pub, + &merge_sig)) + { + GNUNET_break_op (0); + GNUNET_free (payto_uri); + return GNUNET_SYSERR; + } + GNUNET_free (payto_uri); + if (0 == + GNUNET_memcmp (&merge_sig, + cmerge_sig)) + { + /* Must be the SAME data, not a conflict! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} - rh->type = TALER_EXCHANGE_RTT_CLOSE; - rh->amount = amount; - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - closing_spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - TALER_amount_hton (&rcc.closing_amount, - &amount); - GNUNET_CRYPTO_hash ( - rh->details.close_details.receiver_account_details, - strlen (rh->details.close_details.receiver_account_details) + 1, - &rcc.h_wire); - rcc.wtid = rh->details.close_details.wtid; - rcc.purpose.size = htonl (sizeof (rcc)); - rcc.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED); - rcc.reserve_pub = *reserve_pub; - timestamp = GNUNET_TIME_absolute_ntoh (rcc.timestamp); - rh->details.close_details.timestamp = timestamp; - TALER_amount_ntoh (&rh->details.close_details.fee, - &rcc.closing_fee); - key_state = TALER_EXCHANGE_get_keys (exchange); - if (GNUNET_OK != - TALER_EXCHANGE_test_signing_key (key_state, - &rh->details.close_details. - exchange_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED, - &rcc, - &rh->details.close_details.exchange_sig.eddsa_signature, - &rh->details.close_details.exchange_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (&total_out, - &total_out, - &rh->amount)) - { - /* overflow in history already!? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - /* end type==CLOSING */ - } - else - { - /* unexpected 'type', protocol incompatibility, complain! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_coin_conflict_ ( + const struct TALER_PurseContractPublicKeyP *purse_pub, + const char *exchange_url, + const json_t *proof, + struct TALER_DenominationHashP *h_denom_pub, + struct TALER_AgeCommitmentHash *phac, + struct TALER_CoinSpendPublicKeyP *coin_pub, + struct TALER_CoinSpendSignatureP *coin_sig) +{ + const char *partner_url = NULL; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_denom_pub", + h_denom_pub), + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + phac), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + coin_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + coin_pub), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("partner_url", + &partner_url), + NULL), + TALER_JSON_spec_amount_any ("amount", + &amount), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (proof, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; } + if (NULL == partner_url) + partner_url = exchange_url; + if (GNUNET_OK != + TALER_wallet_purse_deposit_verify ( + partner_url, + purse_pub, + &amount, + h_denom_pub, + phac, + coin_pub, + coin_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} - /* check balance = total_in - total_out < withdraw-amount */ - if (0 > - TALER_amount_subtract (balance, - &total_in, - &total_out)) + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_econtract_conflict_ ( + const struct TALER_PurseContractSignatureP *ccontract_sig, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *proof) +{ + struct TALER_ContractDiffiePublicP contract_pub; + struct TALER_PurseContractSignatureP contract_sig; + struct GNUNET_HashCode h_econtract; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_econtract", + &h_econtract), + GNUNET_JSON_spec_fixed_auto ("econtract_sig", + &contract_sig), + GNUNET_JSON_spec_fixed_auto ("contract_pub", + &contract_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (proof, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_econtract_upload_verify2 ( + &h_econtract, + &contract_pub, + purse_pub, + &contract_sig)) { - /* total_in < total_out, why did the exchange ever allow this!? */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == + GNUNET_memcmp (&contract_sig, + ccontract_sig)) + { + /* Must be the SAME data, not a conflict! */ GNUNET_break_op (0); return GNUNET_SYSERR; } @@ -404,581 +275,378 @@ TALER_EXCHANGE_parse_reserve_history ( } -/** - * Free memory (potentially) allocated by #TALER_EXCHANGE_parse_reserve_history(). - * - * @param rhistory result to free - * @param len number of entries in @a rhistory - */ -void -TALER_EXCHANGE_free_reserve_history ( - struct TALER_EXCHANGE_ReserveHistory *rhistory, - unsigned int len) +// FIXME: should be used... +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_denomination_conflict_ ( + const json_t *proof, + const struct TALER_DenominationHashP *ch_denom_pub) { - for (unsigned int i = 0; i<len; i++) + struct TALER_DenominationHashP h_denom_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_denom_pub", + &h_denom_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (proof, + spec, + NULL, NULL)) { - switch (rhistory[i].type) - { - case TALER_EXCHANGE_RTT_CREDIT: - GNUNET_free (rhistory[i].details.in_details.sender_url); - break; - case TALER_EXCHANGE_RTT_WITHDRAWAL: - break; - case TALER_EXCHANGE_RTT_RECOUP: - break; - case TALER_EXCHANGE_RTT_CLOSE: - break; - } + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == + GNUNET_memcmp (ch_denom_pub, + &h_denom_pub)) + { + GNUNET_break_op (0); + return GNUNET_OK; } - GNUNET_free (rhistory); + /* indeed, proof with different denomination key provided */ + return GNUNET_OK; } -/** - * Verify a coins transaction history as returned by the exchange. - * - * @param dk fee structure for the coin, NULL to skip verifying fees - * @param currency expected currency for the coin - * @param coin_pub public key of the coin - * @param history history of the coin in json encoding - * @param[out] h_denom_pub set to the hash of the coin's denomination (if available) - * @param[out] total how much of the coin has been spent according to @a history - * @return #GNUNET_OK if @a history is valid, #GNUNET_SYSERR if not - */ -int -TALER_EXCHANGE_verify_coin_history ( - const struct TALER_EXCHANGE_DenomPublicKey *dk, - const char *currency, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - json_t *history, - struct GNUNET_HashCode *h_denom_pub, - struct TALER_Amount *total) +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_get_min_denomination_ ( + const struct TALER_EXCHANGE_Keys *keys, + struct TALER_Amount *min) { - size_t len; - struct TALER_Amount rtotal; - struct TALER_Amount fee; + bool have_min = false; + for (unsigned int i = 0; i<keys->num_denom_keys; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dk = &keys->denom_keys[i]; - if (NULL == history) + if (! have_min) + { + *min = dk->value; + have_min = true; + continue; + } + if (1 != TALER_amount_cmp (min, + &dk->value)) + continue; + *min = dk->value; + } + if (! have_min) { - GNUNET_break_op (0); + GNUNET_break (0); return GNUNET_SYSERR; } - len = json_array_size (history); - if (0 == len) + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_verify_deposit_signature_ ( + const struct TALER_EXCHANGE_DepositContractDetail *dcd, + const struct TALER_ExtensionPolicyHashP *ech, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_EXCHANGE_CoinDepositDetail *cdd, + const struct TALER_EXCHANGE_DenomPublicKey *dki) +{ + if (GNUNET_OK != + TALER_wallet_deposit_verify (&cdd->amount, + &dki->fees.deposit, + h_wire, + &dcd->h_contract_terms, + &dcd->wallet_data_hash, + &cdd->h_age_commitment, + ech, + &cdd->h_denom_pub, + dcd->wallet_timestamp, + &dcd->merchant_pub, + dcd->refund_deadline, + &cdd->coin_pub, + &cdd->coin_sig)) { GNUNET_break_op (0); + TALER_LOG_WARNING ("Invalid coin signature on /deposit request!\n"); + TALER_LOG_DEBUG ("... amount_with_fee was %s\n", + TALER_amount2s (&cdd->amount)); + TALER_LOG_DEBUG ("... deposit_fee was %s\n", + TALER_amount2s (&dki->fees.deposit)); return GNUNET_SYSERR; } - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (currency, - total)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (currency, - &rtotal)); - for (size_t off = 0; off<len; off++) + + /* check coin signature */ { - int add; - json_t *transaction; - struct TALER_Amount amount; - const char *type; - struct GNUNET_JSON_Specification spec_glob[] = { - TALER_JSON_spec_amount_any ("amount", - &amount), - GNUNET_JSON_spec_string ("type", - &type), - GNUNET_JSON_spec_end () + struct TALER_CoinPublicInfo coin_info = { + .coin_pub = cdd->coin_pub, + .denom_pub_hash = cdd->h_denom_pub, + .denom_sig = cdd->denom_sig, + .h_age_commitment = cdd->h_age_commitment, }; - transaction = json_array_get (history, - off); - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec_glob, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } if (GNUNET_YES != - TALER_amount_cmp_currency (&amount, - &rtotal)) + TALER_test_coin_valid (&coin_info, + &dki->key)) { GNUNET_break_op (0); + TALER_LOG_WARNING ("Invalid coin passed for /deposit\n"); return GNUNET_SYSERR; } - add = GNUNET_SYSERR; - if (0 == strcasecmp (type, - "DEPOSIT")) - { - struct TALER_DepositRequestPS dr = { - .purpose.size = htonl (sizeof (dr)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_DEPOSIT), - .coin_pub = *coin_pub - }; - struct TALER_CoinSpendSignatureP sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &sig), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &dr.h_contract_terms), - GNUNET_JSON_spec_fixed_auto ("h_wire", - &dr.h_wire), - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &dr.h_denom_pub), - TALER_JSON_spec_absolute_time_nbo ("timestamp", - &dr.wallet_timestamp), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_absolute_time_nbo ("refund_deadline", - &dr.refund_deadline)), - TALER_JSON_spec_amount_any_nbo ("deposit_fee", - &dr.deposit_fee), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &dr.merchant), - GNUNET_JSON_spec_end () - }; + } - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - TALER_amount_hton (&dr.amount_with_fee, - &amount); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_DEPOSIT, - &dr, - &sig.eddsa_signature, - &coin_pub->eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - *h_denom_pub = dr.h_denom_pub; - if (NULL != dk) - { - /* check that deposit fee matches our expectations from /keys! */ - TALER_amount_ntoh (&fee, - &dr.deposit_fee); - if ( (GNUNET_YES != - TALER_amount_cmp_currency (&fee, - &dk->fee_deposit)) || - (0 != - TALER_amount_cmp (&fee, - &dk->fee_deposit)) ) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - add = GNUNET_YES; - } - else if (0 == strcasecmp (type, - "MELT")) - { - struct TALER_RefreshMeltCoinAffirmationPS rm; - struct TALER_CoinSpendSignatureP sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &sig), - GNUNET_JSON_spec_fixed_auto ("rc", - &rm.rc), - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &rm.h_denom_pub), - TALER_JSON_spec_amount_any_nbo ("melt_fee", - &rm.melt_fee), - GNUNET_JSON_spec_end () - }; + /* Check coin does make a contribution */ + if (0 < TALER_amount_cmp (&dki->fees.deposit, + &cdd->amount)) + { + GNUNET_break_op (0); + TALER_LOG_WARNING ("Deposit amount smaller than fee\n"); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - rm.purpose.size = htonl (sizeof (rm)); - rm.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_MELT); - TALER_amount_hton (&rm.amount_with_fee, - &amount); - rm.coin_pub = *coin_pub; - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_MELT, - &rm, - &sig.eddsa_signature, - &coin_pub->eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - *h_denom_pub = rm.h_denom_pub; - if (NULL != dk) - { - /* check that melt fee matches our expectations from /keys! */ - TALER_amount_ntoh (&fee, - &rm.melt_fee); - if ( (GNUNET_YES != - TALER_amount_cmp_currency (&fee, - &dk->fee_refresh)) || - (0 != - TALER_amount_cmp (&fee, - &dk->fee_refresh)) ) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - add = GNUNET_YES; - } - else if (0 == strcasecmp (type, - "REFUND")) - { - struct TALER_MerchantSignatureP sig; - struct TALER_Amount refund_fee; - struct TALER_Amount sig_amount; - struct TALER_RefundRequestPS rr = { - .purpose.size = htonl (sizeof (rr)), - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), - .coin_pub = *coin_pub - }; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("refund_fee", - &refund_fee), - GNUNET_JSON_spec_fixed_auto ("merchant_sig", - &sig), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &rr.h_contract_terms), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &rr.merchant), - GNUNET_JSON_spec_uint64 ("rtransaction_id", - &rr.rtransaction_id), - GNUNET_JSON_spec_end () - }; - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (&sig_amount, - &refund_fee, - &amount)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - TALER_amount_hton (&rr.refund_amount, - &sig_amount); - rr.rtransaction_id = GNUNET_htonll (rr.rtransaction_id); - TALER_amount_hton (&rr.refund_amount, - &sig_amount); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_REFUND, - &rr, - &sig.eddsa_sig, - &rr.merchant.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - /* NOTE: theoretically, we could also check that the given - merchant_pub and h_contract_terms appear in the - history under deposits. However, there is really no benefit - for the exchange to lie here, so not checking is probably OK - (an auditor ought to check, though). Then again, we similarly - had no reason to check the merchant's signature (other than a - well-formendess check). */// - - /* check that refund fee matches our expectations from /keys! */ - if (NULL != dk) - { - if ( (GNUNET_YES != - TALER_amount_cmp_currency (&refund_fee, - &dk->fee_refund)) || - (0 != - TALER_amount_cmp (&refund_fee, - &dk->fee_refund)) ) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - add = GNUNET_NO; - } - else if (0 == strcasecmp (type, - "RECOUP")) - { - struct TALER_RecoupConfirmationPS pc = { - .purpose.size = htonl (sizeof (pc)), - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP), - .coin_pub = *coin_pub - }; - struct TALER_RecoupRequestPS rr = { - .purpose.size = htonl (sizeof (pc)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_RECOUP), - .coin_pub = *coin_pub - }; - struct TALER_ExchangePublicKeyP exchange_pub; - struct TALER_ExchangeSignatureP exchange_sig; - struct TALER_CoinSpendSignatureP coin_sig; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any_nbo ("amount", - &pc.recoup_amount), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &exchange_pub), - GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &pc.reserve_pub), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_spec_fixed_auto ("coin_blind", - &rr.coin_blind), - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &rr.h_denom_pub), - TALER_JSON_spec_absolute_time_nbo ("timestamp", - &pc.timestamp), - GNUNET_JSON_spec_end () - }; +/** + * Parse account restriction in @a jrest into @a rest. + * + * @param jresta array of account restrictions in JSON + * @param[out] resta_len set to length of @a resta + * @param[out] resta account restriction array to set + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_restrictions (const json_t *jresta, + unsigned int *resta_len, + struct TALER_EXCHANGE_AccountRestriction **resta) +{ + size_t alen; - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - TALER_amount_hton (&pc.recoup_amount, - &amount); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP, - &pc, - &exchange_sig.eddsa_signature, - &exchange_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_RECOUP, - &rr, - &coin_sig.eddsa_signature, - &coin_pub->eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - *h_denom_pub = rr.h_denom_pub; - add = GNUNET_YES; + if (! json_is_array (jresta)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + alen = json_array_size (jresta); + if (0 == alen) + { + /* no restrictions, perfectly OK */ + *resta = NULL; + return GNUNET_OK; + } + *resta_len = (unsigned int) alen; + GNUNET_assert (alen == *resta_len); + *resta = GNUNET_new_array (*resta_len, + struct TALER_EXCHANGE_AccountRestriction); + for (unsigned int i = 0; i<*resta_len; i++) + { + const json_t *jr = json_array_get (jresta, + i); + struct TALER_EXCHANGE_AccountRestriction *ar = &(*resta)[i]; + const char *type = json_string_value (json_object_get (jr, + "type")); + + if (NULL == type) + { + GNUNET_break (0); + goto fail; } - else if (0 == strcasecmp (type, - "RECOUP-REFRESH")) + if (0 == strcmp (type, + "deny")) { - struct TALER_RecoupRefreshConfirmationPS pc = { - .purpose.size = htonl (sizeof (pc)), - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH), - .coin_pub = *coin_pub - }; - struct TALER_RecoupRequestPS rr = { - .purpose.size = htonl (sizeof (pc)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_RECOUP), - .coin_pub = *coin_pub - }; - struct TALER_ExchangePublicKeyP exchange_pub; - struct TALER_ExchangeSignatureP exchange_sig; - struct TALER_CoinSpendSignatureP coin_sig; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any_nbo ("amount", - &pc.recoup_amount), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &exchange_pub), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_spec_fixed_auto ("old_coin_pub", - &pc.old_coin_pub), - GNUNET_JSON_spec_fixed_auto ("coin_blind", - &rr.coin_blind), - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &rr.h_denom_pub), - TALER_JSON_spec_absolute_time_nbo ("timestamp", - &pc.timestamp), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - TALER_amount_hton (&pc.recoup_amount, - &amount); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH, - &pc, - &exchange_sig.eddsa_signature, - &exchange_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_RECOUP, - &rr, - &coin_sig.eddsa_signature, - &coin_pub->eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - *h_denom_pub = rr.h_denom_pub; - add = GNUNET_YES; + ar->type = TALER_EXCHANGE_AR_DENY; + continue; } - else if (0 == strcasecmp (type, - "OLD-COIN-RECOUP")) + if (0 == strcmp (type, + "regex")) { - struct TALER_RecoupRefreshConfirmationPS pc = { - .purpose.size = htonl (sizeof (pc)), - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH), - .old_coin_pub = *coin_pub - }; - struct TALER_ExchangePublicKeyP exchange_pub; - struct TALER_ExchangeSignatureP exchange_sig; + const char *regex; + const char *hint; struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any_nbo ("amount", - &pc.recoup_amount), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &exchange_pub), - GNUNET_JSON_spec_fixed_auto ("coin_pub", - &pc.coin_pub), - TALER_JSON_spec_absolute_time_nbo ("timestamp", - &pc.timestamp), + GNUNET_JSON_spec_string ( + "payto_regex", + ®ex), + GNUNET_JSON_spec_string ( + "human_hint", + &hint), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ( + "human_hint_i18n", + &ar->details.regex.human_hint_i18n), + NULL), GNUNET_JSON_spec_end () }; if (GNUNET_OK != - GNUNET_JSON_parse (transaction, + GNUNET_JSON_parse (jr, spec, NULL, NULL)) { + /* bogus reply */ GNUNET_break_op (0); - return GNUNET_SYSERR; + goto fail; } - TALER_amount_hton (&pc.recoup_amount, - &amount); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH, - &pc, - &exchange_sig.eddsa_signature, - &exchange_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - add = GNUNET_YES; + ar->type = TALER_EXCHANGE_AR_REGEX; + ar->details.regex.posix_egrep = GNUNET_strdup (regex); + ar->details.regex.human_hint = GNUNET_strdup (hint); + continue; } - else + /* unsupported type */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +fail: + GNUNET_free (*resta); + *resta_len = 0; + return GNUNET_SYSERR; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_parse_accounts ( + const struct TALER_MasterPublicKeyP *master_pub, + const json_t *accounts, + unsigned int was_length, + struct TALER_EXCHANGE_WireAccount was[static was_length]) +{ + memset (was, + 0, + sizeof (struct TALER_EXCHANGE_WireAccount) * was_length); + GNUNET_assert (was_length == + json_array_size (accounts)); + for (unsigned int i = 0; + i<was_length; + i++) + { + struct TALER_EXCHANGE_WireAccount *wa = &was[i]; + const char *payto_uri; + const char *conversion_url = NULL; + const char *bank_label = NULL; + int64_t priority = 0; + const json_t *credit_restrictions; + const json_t *debit_restrictions; + struct GNUNET_JSON_Specification spec_account[] = { + TALER_JSON_spec_payto_uri ("payto_uri", + &payto_uri), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("conversion_url", + &conversion_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_int64 ("priority", + &priority), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("bank_label", + &bank_label), + NULL), + GNUNET_JSON_spec_array_const ("credit_restrictions", + &credit_restrictions), + GNUNET_JSON_spec_array_const ("debit_restrictions", + &debit_restrictions), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &wa->master_sig), + GNUNET_JSON_spec_end () + }; + json_t *account; + + account = json_array_get (accounts, + i); + if (GNUNET_OK != + GNUNET_JSON_parse (account, + spec_account, + NULL, NULL)) { - /* signature not supported, new version on server? */ + /* bogus reply */ GNUNET_break_op (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected type `%s' in response\n", - type); - GNUNET_assert (GNUNET_SYSERR == add); return GNUNET_SYSERR; } - - if (GNUNET_YES == add) + if ( (NULL != master_pub) && + (GNUNET_OK != + TALER_exchange_wire_signature_check ( + payto_uri, + conversion_url, + debit_restrictions, + credit_restrictions, + master_pub, + &wa->master_sig)) ) { - /* This amount should be added to the total */ - if (0 > - TALER_amount_add (total, - total, - &amount)) - { - /* overflow in history already!? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } + /* bogus reply */ + GNUNET_break_op (0); + return GNUNET_SYSERR; } - else + if ( (GNUNET_OK != + parse_restrictions (credit_restrictions, + &wa->credit_restrictions_length, + &wa->credit_restrictions)) || + (GNUNET_OK != + parse_restrictions (debit_restrictions, + &wa->debit_restrictions_length, + &wa->debit_restrictions)) ) { - /* This amount should be subtracted from the total. - - However, for the implementation, we first *add* up all of - these negative amounts, as we might get refunds before - deposits from a semi-evil exchange. Then, at the end, we do - the subtraction by calculating "total = total - rtotal" */GNUNET_assert (GNUNET_NO == add); - if (0 > - TALER_amount_add (&rtotal, - &rtotal, - &amount)) - { - /* overflow in refund history? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - + /* bogus reply */ + GNUNET_break_op (0); + return GNUNET_SYSERR; } - } - - - /* Finally, subtract 'rtotal' from total to handle the subtractions */ - if (0 > - TALER_amount_subtract (total, - total, - &rtotal)) - { - /* underflow in history? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - + wa->payto_uri = GNUNET_strdup (payto_uri); + wa->priority = priority; + if (NULL != conversion_url) + wa->conversion_url = GNUNET_strdup (conversion_url); + if (NULL != bank_label) + wa->bank_label = GNUNET_strdup (bank_label); + } /* end 'for all accounts */ return GNUNET_OK; } /** - * Obtain meta data about an exchange (online) signing - * key. + * Free array of account restrictions. * - * @param keys from where to obtain the meta data - * @param exchange_pub public key to lookup - * @return NULL on error (@a exchange_pub not known) + * @param ar_len length of @a ar + * @param[in] ar array to free contents of (but not @a ar itself) */ -const struct TALER_EXCHANGE_SigningPublicKey * -TALER_EXCHANGE_get_signing_key_info ( - const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_ExchangePublicKeyP *exchange_pub) +static void +free_restrictions (unsigned int ar_len, + struct TALER_EXCHANGE_AccountRestriction ar[static ar_len]) { - for (unsigned int i = 0; i<keys->num_sign_keys; i++) + for (unsigned int i = 0; i<ar_len; i++) { - const struct TALER_EXCHANGE_SigningPublicKey *spk - = &keys->sign_keys[i]; + struct TALER_EXCHANGE_AccountRestriction *a = &ar[i]; + switch (a->type) + { + case TALER_EXCHANGE_AR_INVALID: + GNUNET_break (0); + break; + case TALER_EXCHANGE_AR_DENY: + break; + case TALER_EXCHANGE_AR_REGEX: + GNUNET_free (ar->details.regex.posix_egrep); + GNUNET_free (ar->details.regex.human_hint); + json_decref (ar->details.regex.human_hint_i18n); + break; + } + } +} - if (0 == GNUNET_memcmp (exchange_pub, - &spk->key)) - return spk; + +void +TALER_EXCHANGE_free_accounts ( + unsigned int was_len, + struct TALER_EXCHANGE_WireAccount was[static was_len]) +{ + for (unsigned int i = 0; i<was_len; i++) + { + struct TALER_EXCHANGE_WireAccount *wa = &was[i]; + + GNUNET_free (wa->payto_uri); + GNUNET_free (wa->conversion_url); + GNUNET_free (wa->bank_label); + free_restrictions (wa->credit_restrictions_length, + wa->credit_restrictions); + GNUNET_array_grow (wa->credit_restrictions, + wa->credit_restrictions_length, + 0); + free_restrictions (wa->debit_restrictions_length, + wa->debit_restrictions); + GNUNET_array_grow (wa->debit_restrictions, + wa->debit_restrictions_length, + 0); } - return NULL; } diff --git a/src/lib/exchange_api_common.h b/src/lib/exchange_api_common.h new file mode 100644 index 000000000..f1f0fd7fa --- /dev/null +++ b/src/lib/exchange_api_common.h @@ -0,0 +1,180 @@ +/* + This file is part of TALER + Copyright (C) 2015-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_common.h + * @brief common functions for the exchange API + * @author Christian Grothoff + */ +#ifndef EXCHANGE_API_COMMON_H +#define EXCHANGE_API_COMMON_H + +#include "taler_json_lib.h" +#include "taler_exchange_service.h" + + +/** + * Check proof of a purse creation conflict. + * + * @param cpurse_sig conflicting signature (must + * not match the signature from the proof) + * @param purse_pub the public key (must match + * the signature from the proof) + * @param proof the proof to check + * @return #GNUNET_OK if the @a proof is OK for @a purse_pub and conflicts with @a cpurse_sig + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_create_conflict_ ( + const struct TALER_PurseContractSignatureP *cpurse_sig, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *proof); + + +/** + * Check proof of a purse merge conflict. + * + * @param cmerge_sig conflicting signature (must + * not match the signature from the proof) + * @param merge_pub the public key (must match + * the signature from the proof) + * @param purse_pub the public key of the purse + * @param exchange_url the base URL of this exchange + * @param proof the proof to check + * @return #GNUNET_OK if the @a proof is OK for @a purse_pub and @a merge_pub and conflicts with @a cmerge_sig + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_merge_conflict_ ( + const struct TALER_PurseMergeSignatureP *cmerge_sig, + const struct TALER_PurseMergePublicKeyP *merge_pub, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const char *exchange_url, + const json_t *proof); + + +/** + * Check @a proof that claims this coin was spend + * differently on the same purse already. Note that + * the caller must still check that @a coin_pub is + * in the list of coins that were used, and that + * @a coin_sig is different from the signature the + * caller used. + * + * @param purse_pub the public key of the purse + * @param exchange_url base URL of our exchange + * @param proof the proof to check + * @param[out] h_denom_pub hash of the coin's denomination + * @param[out] phac age commitment hash of the coin + * @param[out] coin_pub set to the conflicting coin + * @param[out] coin_sig set to the conflicting signature + * @return #GNUNET_OK if the @a proof is OK for @a purse_pub and showing that @a coin_pub was spent using @a coin_sig. + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_coin_conflict_ ( + const struct TALER_PurseContractPublicKeyP *purse_pub, + const char *exchange_url, + const json_t *proof, + struct TALER_DenominationHashP *h_denom_pub, + struct TALER_AgeCommitmentHash *phac, + struct TALER_CoinSpendPublicKeyP *coin_pub, + struct TALER_CoinSpendSignatureP *coin_sig); + + +/** + * Check proof of a contract conflict. + * + * @param ccontract_sig conflicting signature (must + * not match the signature from the proof) + * @param purse_pub public key of the purse + * @param proof the proof to check + * @return #GNUNET_OK if the @a proof is OK for @a purse_pub and conflicts with @a ccontract_sig + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_purse_econtract_conflict_ ( + const struct TALER_PurseContractSignatureP *ccontract_sig, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *proof); + + +/** + * Check proof of a coin spend value conflict. + * + * @param keys exchange /keys structure + * @param proof the proof to check + * @param[out] coin_pub set to the public key of the + * coin that is claimed to have an insufficient + * balance + * @param[out] remaining set to the remaining balance + * of the coin as provided by the proof + * @return #GNUNET_OK if the @a proof is OK for @a purse_pub demonstrating that @a coin_pub has only @a remaining balance. + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_amount_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + const json_t *proof, + struct TALER_CoinSpendPublicKeyP *coin_pub, + struct TALER_Amount *remaining); + + +/** + * Verify that @a proof contains a coin history that demonstrates that @a + * coin_pub was previously used with a denomination key that is different from + * @a ch_denom_pub. Note that the coin history MUST have been checked before + * using #TALER_EXCHANGE_check_coin_amount_conflict_(). + * + * @param proof a proof to check + * @param ch_denom_pub hash of the conflicting denomination + * @return #GNUNET_OK if @a ch_denom_pub differs from the + * denomination hash given by the history of the coin + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_denomination_conflict_ ( + const json_t *proof, + const struct TALER_DenominationHashP *ch_denom_pub); + + +/** + * Find the smallest denomination amount in @e keys. + * + * @param keys keys to search + * @param[out] min set to the smallest amount + * @return #GNUNET_SYSERR if there are no denominations in @a keys + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_get_min_denomination_ ( + const struct TALER_EXCHANGE_Keys *keys, + struct TALER_Amount *min); + + +/** + * Verify signature information about the deposit. + * + * @param dcd contract details + * @param ech hashed policy (passed to avoid recomputation) + * @param h_wire hashed wire details (passed to avoid recomputation) + * @param cdd coin-specific details + * @param dki denomination of the coin + * @return #GNUNET_OK if signatures are OK, #GNUNET_SYSERR if not + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_verify_deposit_signature_ ( + const struct TALER_EXCHANGE_DepositContractDetail *dcd, + const struct TALER_ExtensionPolicyHashP *ech, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_EXCHANGE_CoinDepositDetail *cdd, + const struct TALER_EXCHANGE_DenomPublicKey *dki); + + +#endif diff --git a/src/lib/exchange_api_contracts_get.c b/src/lib/exchange_api_contracts_get.c new file mode 100644 index 000000000..aece7733a --- /dev/null +++ b/src/lib/exchange_api_contracts_get.c @@ -0,0 +1,262 @@ +/* + This file is part of TALER + Copyright (C) 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_contracts_get.c + * @brief Implementation of the /contracts/ GET request + * @author Christian Grothoff + */ +#include "platform.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 "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * @brief A Contract Get Handle + */ +struct TALER_EXCHANGE_ContractsGetHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ContractGetCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Private key needed to decrypt the contract. + */ + struct TALER_ContractDiffiePrivateP contract_priv; + + /** + * Public key matching @e contract_priv. + */ + struct TALER_ContractDiffiePublicP cpub; + +}; + + +/** + * Function called when we're done processing the + * HTTP /track/transaction request. + * + * @param cls the `struct TALER_EXCHANGE_ContractsGetHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_contract_get_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ContractsGetHandle *cgh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_ContractGetResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + cgh->job = NULL; + switch (response_code) + { + case 0: + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + void *econtract; + size_t econtract_size; + struct TALER_PurseContractSignatureP econtract_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("purse_pub", + &dr.details.ok.purse_pub), + GNUNET_JSON_spec_fixed_auto ("econtract_sig", + &econtract_sig), + GNUNET_JSON_spec_varsize ("econtract", + &econtract, + &econtract_size), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (GNUNET_OK != + TALER_wallet_econtract_upload_verify ( + econtract, + econtract_size, + &cgh->cpub, + &dr.details.ok.purse_pub, + &econtract_sig)) + { + GNUNET_break (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_CONTRACTS_SIGNATURE_INVALID; + GNUNET_JSON_parse_free (spec); + break; + } + dr.details.ok.econtract = econtract; + dr.details.ok.econtract_size = econtract_size; + cgh->cb (cgh->cb_cls, + &dr); + GNUNET_JSON_parse_free (spec); + TALER_EXCHANGE_contract_get_cancel (cgh); + return; + } + case MHD_HTTP_BAD_REQUEST: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Exchange does not know about transaction; + we should pass the reply to the application */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange GET contracts\n", + (unsigned int) response_code, + (int) dr.hr.ec); + GNUNET_break_op (0); + break; + } + cgh->cb (cgh->cb_cls, + &dr); + TALER_EXCHANGE_contract_get_cancel (cgh); +} + + +struct TALER_EXCHANGE_ContractsGetHandle * +TALER_EXCHANGE_contract_get ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_ContractDiffiePrivateP *contract_priv, + TALER_EXCHANGE_ContractGetCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ContractsGetHandle *cgh; + CURL *eh; + char arg_str[sizeof (cgh->cpub) * 2 + 48]; + + cgh = GNUNET_new (struct TALER_EXCHANGE_ContractsGetHandle); + cgh->cb = cb; + cgh->cb_cls = cb_cls; + GNUNET_CRYPTO_ecdhe_key_get_public (&contract_priv->ecdhe_priv, + &cgh->cpub.ecdhe_pub); + { + char cpub_str[sizeof (cgh->cpub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string (&cgh->cpub, + sizeof (cgh->cpub), + cpub_str, + sizeof (cpub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "contracts/%s", + cpub_str); + } + + cgh->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == cgh->url) + { + GNUNET_free (cgh); + return NULL; + } + cgh->contract_priv = *contract_priv; + + eh = TALER_EXCHANGE_curl_easy_get_ (cgh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (cgh->url); + GNUNET_free (cgh); + return NULL; + } + cgh->job = GNUNET_CURL_job_add (ctx, + eh, + &handle_contract_get_finished, + cgh); + return cgh; +} + + +void +TALER_EXCHANGE_contract_get_cancel ( + struct TALER_EXCHANGE_ContractsGetHandle *cgh) +{ + if (NULL != cgh->job) + { + GNUNET_CURL_job_cancel (cgh->job); + cgh->job = NULL; + } + GNUNET_free (cgh->url); + GNUNET_free (cgh); +} + + +/* end of exchange_api_contracts_get.c */ diff --git a/src/lib/exchange_api_csr_melt.c b/src/lib/exchange_api_csr_melt.c new file mode 100644 index 000000000..bf6f4bfe1 --- /dev/null +++ b/src/lib/exchange_api_csr_melt.c @@ -0,0 +1,320 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_csr_melt.c + * @brief Implementation of /csr-melt requests (get R in exchange used for Clause Schnorr refresh) + * @author Lucien Heuzeveldt + * @author Gian Demarmels + */ +#include "platform.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 "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 Clause Schnorr R Handle + */ +struct TALER_EXCHANGE_CsRMeltHandle +{ + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_CsRMeltCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; +}; + + +/** + * We got a 200 OK response for the /reserves/$RESERVE_PUB/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 csrh operation handle + * @param arr reply from the exchange + * @param hr http response details + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +csr_ok (struct TALER_EXCHANGE_CsRMeltHandle *csrh, + const json_t *arr, + struct TALER_EXCHANGE_HttpResponse *hr) +{ + size_t alen = json_array_size (arr); + struct TALER_ExchangeWithdrawValues alg_values[GNUNET_NZL (alen)]; + struct TALER_EXCHANGE_CsRMeltResponse csrr = { + .hr = *hr, + .details.ok.alg_values_len = alen, + .details.ok.alg_values = alg_values + }; + + for (size_t i = 0; i<alen; i++) + { + json_t *av = json_array_get (arr, + i); + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_exchange_withdraw_values ( + "ewv", + &alg_values[i]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (av, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + csrh->cb (csrh->cb_cls, + &csrr); + for (size_t i = 0; i<alen; i++) + TALER_denom_ewv_free (&alg_values[i]); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the HTTP /csr request. + * + * @param cls the `struct TALER_EXCHANGE_CsRMeltHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_csr_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_CsRMeltHandle *csrh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_HttpResponse hr = { + .reply = j, + .http_status = (unsigned int) response_code + }; + struct TALER_EXCHANGE_CsRMeltResponse csrr = { + .hr = hr + }; + + csrh->job = NULL; + switch (response_code) + { + case 0: + csrr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + json_t *arr; + + arr = json_object_get (j, + "ewvs"); + if ( (NULL == arr) || + (0 == json_array_size (arr)) || + (GNUNET_OK != + csr_ok (csrh, + arr, + &hr)) ) + { + GNUNET_break_op (0); + csrr.hr.http_status = 0; + csrr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + TALER_EXCHANGE_csr_melt_cancel (csrh); + 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 */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.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 the /csr endpoint or denomination. + Can happen if the exchange doesn't support Clause Schnorr. + We should simply pass the JSON reply to the application. */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.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 */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.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 */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for CS R request\n", + (unsigned int) response_code, + (int) hr.ec); + break; + } + csrh->cb (csrh->cb_cls, + &csrr); + csrh->cb = NULL; + TALER_EXCHANGE_csr_melt_cancel (csrh); +} + + +struct TALER_EXCHANGE_CsRMeltHandle * +TALER_EXCHANGE_csr_melt ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_RefreshMasterSecretP *rms, + unsigned int nks_len, + struct TALER_EXCHANGE_NonceKey nks[static nks_len], + TALER_EXCHANGE_CsRMeltCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_CsRMeltHandle *csrh; + json_t *csr_arr; + + if (0 == nks_len) + { + GNUNET_break (0); + return NULL; + } + for (unsigned int i = 0; i<nks_len; i++) + if (GNUNET_CRYPTO_BSA_CS != + nks[i].pk->key.bsign_pub_key->cipher) + { + GNUNET_break (0); + return NULL; + } + csrh = GNUNET_new (struct TALER_EXCHANGE_CsRMeltHandle); + csrh->cb = res_cb; + csrh->cb_cls = res_cb_cls; + csr_arr = json_array (); + GNUNET_assert (NULL != csr_arr); + for (unsigned int i = 0; i<nks_len; i++) + { + const struct TALER_EXCHANGE_NonceKey *nk = &nks[i]; + json_t *csr_obj; + + csr_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("coin_offset", + nk->cnc_num), + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &nk->pk->h_key)); + GNUNET_assert (NULL != csr_obj); + GNUNET_assert (0 == + json_array_append_new (csr_arr, + csr_obj)); + } + csrh->url = TALER_url_join (url, + "csr-melt", + NULL); + if (NULL == csrh->url) + { + json_decref (csr_arr); + GNUNET_free (csrh); + return NULL; + } + { + CURL *eh; + json_t *req; + + req = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("rms", + rms), + GNUNET_JSON_pack_array_steal ("nks", + csr_arr)); + eh = TALER_EXCHANGE_curl_easy_get_ (csrh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&csrh->post_ctx, + eh, + req)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (req); + GNUNET_free (csrh->url); + GNUNET_free (csrh); + return NULL; + } + json_decref (req); + csrh->job = GNUNET_CURL_job_add2 (ctx, + eh, + csrh->post_ctx.headers, + &handle_csr_finished, + csrh); + } + return csrh; +} + + +void +TALER_EXCHANGE_csr_melt_cancel (struct TALER_EXCHANGE_CsRMeltHandle *csrh) +{ + if (NULL != csrh->job) + { + GNUNET_CURL_job_cancel (csrh->job); + csrh->job = NULL; + } + GNUNET_free (csrh->url); + TALER_curl_easy_post_finished (&csrh->post_ctx); + GNUNET_free (csrh); +} diff --git a/src/lib/exchange_api_csr_withdraw.c b/src/lib/exchange_api_csr_withdraw.c new file mode 100644 index 000000000..0fe731cd5 --- /dev/null +++ b/src/lib/exchange_api_csr_withdraw.c @@ -0,0 +1,281 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_csr_withdraw.c + * @brief Implementation of /csr-withdraw requests (get R in exchange used for Clause Schnorr withdraw and refresh) + * @author Lucien Heuzeveldt + * @author Gian Demarmels + */ +#include "platform.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 "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 Clause Schnorr R Handle + */ +struct TALER_EXCHANGE_CsRWithdrawHandle +{ + /** + * Function to call with the result. + */ + TALER_EXCHANGE_CsRWithdrawCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; +}; + + +/** + * We got a 200 OK response for the /reserves/$RESERVE_PUB/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 csrh operation handle + * @param av reply from the exchange + * @param hr http response details + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +csr_ok (struct TALER_EXCHANGE_CsRWithdrawHandle *csrh, + const json_t *av, + struct TALER_EXCHANGE_HttpResponse *hr) +{ + struct TALER_EXCHANGE_CsRWithdrawResponse csrr = { + .hr = *hr, + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_exchange_withdraw_values ( + "ewv", + &csrr.details.ok.alg_values), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (av, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + csrh->cb (csrh->cb_cls, + &csrr); + TALER_denom_ewv_free (&csrr.details.ok.alg_values); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the HTTP /csr request. + * + * @param cls the `struct TALER_EXCHANGE_CsRWithdrawHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_csr_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_CsRWithdrawHandle *csrh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_HttpResponse hr = { + .reply = j, + .http_status = (unsigned int) response_code + }; + struct TALER_EXCHANGE_CsRWithdrawResponse csrr = { + .hr = hr + }; + + csrh->job = NULL; + switch (response_code) + { + case 0: + csrr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + if (GNUNET_OK != + csr_ok (csrh, + response, + &hr)) + { + GNUNET_break_op (0); + csrr.hr.http_status = 0; + csrr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + TALER_EXCHANGE_csr_withdraw_cancel (csrh); + 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 */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.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 the /csr endpoint or denomination. + Can happen if the exchange doesn't support Clause Schnorr. + We should simply pass the JSON reply to the application. */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.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 */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.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 */ + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + csrr.hr.ec = TALER_JSON_get_error_code (j); + csrr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for CS R request\n", + (unsigned int) response_code, + (int) hr.ec); + break; + } + csrh->cb (csrh->cb_cls, + &csrr); + csrh->cb = NULL; + TALER_EXCHANGE_csr_withdraw_cancel (csrh); +} + + +struct TALER_EXCHANGE_CsRWithdrawHandle * +TALER_EXCHANGE_csr_withdraw ( + struct GNUNET_CURL_Context *curl_ctx, + const char *exchange_url, + const struct TALER_EXCHANGE_DenomPublicKey *pk, + const struct GNUNET_CRYPTO_CsSessionNonce *nonce, + TALER_EXCHANGE_CsRWithdrawCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_CsRWithdrawHandle *csrh; + + if (GNUNET_CRYPTO_BSA_CS != + pk->key.bsign_pub_key->cipher) + { + GNUNET_break (0); + return NULL; + } + csrh = GNUNET_new (struct TALER_EXCHANGE_CsRWithdrawHandle); + csrh->cb = res_cb; + csrh->cb_cls = res_cb_cls; + csrh->url = TALER_url_join (exchange_url, + "csr-withdraw", + NULL); + if (NULL == csrh->url) + { + GNUNET_free (csrh); + return NULL; + } + + { + CURL *eh; + json_t *req; + + req = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_varsize ("nonce", + nonce, + sizeof(*nonce)), + GNUNET_JSON_pack_data_varsize ("denom_pub_hash", + &pk->h_key, + sizeof(pk->h_key))); + GNUNET_assert (NULL != req); + eh = TALER_EXCHANGE_curl_easy_get_ (csrh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&csrh->post_ctx, + eh, + req)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (req); + GNUNET_free (csrh->url); + GNUNET_free (csrh); + return NULL; + } + json_decref (req); + csrh->job = GNUNET_CURL_job_add2 (curl_ctx, + eh, + csrh->post_ctx.headers, + &handle_csr_finished, + csrh); + } + return csrh; +} + + +void +TALER_EXCHANGE_csr_withdraw_cancel ( + struct TALER_EXCHANGE_CsRWithdrawHandle *csrh) +{ + if (NULL != csrh->job) + { + GNUNET_CURL_job_cancel (csrh->job); + csrh->job = NULL; + } + GNUNET_free (csrh->url); + TALER_curl_easy_post_finished (&csrh->post_ctx); + GNUNET_free (csrh); +} diff --git a/src/lib/exchange_api_curl_defaults.c b/src/lib/exchange_api_curl_defaults.c index 82d3ace13..85a32189b 100644 --- a/src/lib/exchange_api_curl_defaults.c +++ b/src/lib/exchange_api_curl_defaults.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2018 Taler Systems SA + Copyright (C) 2014-2018, 2021 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 @@ -19,16 +19,11 @@ * @brief curl easy handle defaults * @author Florian Dold */ - +#include "platform.h" +#include "taler_curl_lib.h" #include "exchange_api_curl_defaults.h" -/** - * Get a curl handle with the right defaults for the exchange lib. In the - * future, we might manage a pool of connections here. - * - * @param url URL to query - */ CURL * TALER_EXCHANGE_curl_easy_get_ (const char *url) { @@ -36,21 +31,22 @@ TALER_EXCHANGE_curl_easy_get_ (const char *url) eh = curl_easy_init (); if (NULL == eh) + { + GNUNET_break (0); return NULL; + } GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_URL, url)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_FOLLOWLOCATION, - 1L)); - /* limit MAXREDIRS to 5 as a simple security measure against - a potential infinite loop caused by a malicious target */ - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_MAXREDIRS, - 5L)); + TALER_curl_set_secure_redirect_policy (eh, + url); + /* Enable compression (using whatever curl likes), see + https://curl.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html */ + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_ACCEPT_ENCODING, + "")); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_TCP_FASTOPEN, diff --git a/src/lib/exchange_api_curl_defaults.h b/src/lib/exchange_api_curl_defaults.h index 009d72ab8..c4ba04fc5 100644 --- a/src/lib/exchange_api_curl_defaults.h +++ b/src/lib/exchange_api_curl_defaults.h @@ -25,7 +25,6 @@ #define _TALER_CURL_DEFAULTS_H -#include "platform.h" #include <gnunet/gnunet_curl_lib.h> diff --git a/src/lib/exchange_api_deposit.c b/src/lib/exchange_api_deposit.c deleted file mode 100644 index 86f5034aa..000000000 --- a/src/lib/exchange_api_deposit.c +++ /dev/null @@ -1,773 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2021 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_deposit.c - * @brief Implementation of the /deposit request of the exchange's HTTP API - * @author Sree Harsha Totakura <sreeharsha@totakura.in> - * @author Christian Grothoff - */ -#include "platform.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 "taler_json_lib.h" -#include "taler_auditor_service.h" -#include "taler_exchange_service.h" -#include "exchange_api_handle.h" -#include "taler_signatures.h" -#include "exchange_api_curl_defaults.h" - - -/** - * 1:#AUDITOR_CHANCE is the probability that we report deposits - * to the auditor. - * - * 20==5% of going to auditor. This is possibly still too high, but set - * deliberately this high for testing - */ -#define AUDITOR_CHANCE 20 - -/** - * @brief A Deposit Handle - */ -struct TALER_EXCHANGE_DepositHandle -{ - - /** - * The connection to exchange this request handle will use - */ - struct TALER_EXCHANGE_Handle *exchange; - - /** - * The url for this request. - */ - char *url; - - /** - * Context for #TEH_curl_easy_post(). Keeps the data that must - * persist for Curl to make the upload. - */ - struct TALER_CURL_PostContext ctx; - - /** - * Handle for the request. - */ - struct GNUNET_CURL_Job *job; - - /** - * Function to call with the result. - */ - TALER_EXCHANGE_DepositResultCallback cb; - - /** - * Closure for @a cb. - */ - void *cb_cls; - - /** - * Information the exchange should sign in response. - */ - struct TALER_DepositConfirmationPS depconf; - - /** - * Exchange signature, set for #auditor_cb. - */ - struct TALER_ExchangeSignatureP exchange_sig; - - /** - * Exchange signing public key, set for #auditor_cb. - */ - struct TALER_ExchangePublicKeyP exchange_pub; - - /** - * Value of the /deposit transaction, including fee. - */ - struct TALER_Amount amount_with_fee; - - /** - * @brief Public information about the coin's denomination key. - * Note that the "key" field itself has been zero'ed out. - */ - struct TALER_EXCHANGE_DenomPublicKey dki; - - /** - * Chance that we will inform the auditor about the deposit - * is 1:n, where the value of this field is "n". - */ - unsigned int auditor_chance; - -}; - - -/** - * Function called for each auditor to give us a chance to possibly - * launch a deposit confirmation interaction. - * - * @param cls closure - * @param ah handle to the auditor - * @param auditor_pub public key of the auditor - * @return NULL if no deposit confirmation interaction was launched - */ -static struct TEAH_AuditorInteractionEntry * -auditor_cb (void *cls, - struct TALER_AUDITOR_Handle *ah, - const struct TALER_AuditorPublicKeyP *auditor_pub) -{ - struct TALER_EXCHANGE_DepositHandle *dh = cls; - const struct TALER_EXCHANGE_Keys *key_state; - const struct TALER_EXCHANGE_SigningPublicKey *spk; - struct TALER_Amount amount_without_fee; - struct TEAH_AuditorInteractionEntry *aie; - - if (0 != GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_WEAK, - dh->auditor_chance)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Not providing deposit confirmation to auditor\n"); - return NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Will provide deposit confirmation to auditor `%s'\n", - TALER_B2S (auditor_pub)); - key_state = TALER_EXCHANGE_get_keys (dh->exchange); - spk = TALER_EXCHANGE_get_signing_key_info (key_state, - &dh->exchange_pub); - if (NULL == spk) - { - GNUNET_break_op (0); - return NULL; - } - TALER_amount_ntoh (&amount_without_fee, - &dh->depconf.amount_without_fee); - aie = GNUNET_new (struct TEAH_AuditorInteractionEntry); - aie->dch = TALER_AUDITOR_deposit_confirmation ( - ah, - &dh->depconf.h_wire, - &dh->depconf.h_contract_terms, - GNUNET_TIME_absolute_ntoh (dh->depconf.exchange_timestamp), - GNUNET_TIME_absolute_ntoh (dh->depconf.refund_deadline), - &amount_without_fee, - &dh->depconf.coin_pub, - &dh->depconf.merchant, - &dh->exchange_pub, - &dh->exchange_sig, - &key_state->master_pub, - spk->valid_from, - spk->valid_until, - spk->valid_legal, - &spk->master_sig, - &TEAH_acc_confirmation_cb, - aie); - return aie; -} - - -/** - * Verify that the signature on the "200 OK" response - * from the exchange is valid. - * - * @param dh deposit handle - * @param json json reply with the signature - * @param[out] exchange_sig set to the exchange's signature - * @param[out] exchange_pub set to the exchange's public key - * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not - */ -static int -verify_deposit_signature_ok (struct TALER_EXCHANGE_DepositHandle *dh, - const json_t *json, - struct TALER_ExchangeSignatureP *exchange_sig, - struct TALER_ExchangePublicKeyP *exchange_pub) -{ - const struct TALER_EXCHANGE_Keys *key_state; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("exchange_sig", exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", exchange_pub), - TALER_JSON_spec_absolute_time_nbo ("exchange_timestamp", - &dh->depconf.exchange_timestamp), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (json, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - key_state = TALER_EXCHANGE_get_keys (dh->exchange); - if (GNUNET_OK != - TALER_EXCHANGE_test_signing_key (key_state, - exchange_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT, - &dh->depconf, - &exchange_sig->eddsa_signature, - &exchange_pub->eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - dh->exchange_sig = *exchange_sig; - dh->exchange_pub = *exchange_pub; - TEAH_get_auditors_for_dc (dh->exchange, - &auditor_cb, - dh); - return GNUNET_OK; -} - - -/** - * Verify that the signatures on the "403 FORBIDDEN" response from the - * exchange demonstrating customer double-spending are valid. - * - * @param dh deposit handle - * @param json json reply with the signature(s) and transaction history - * @return #GNUNET_OK if the signature(s) is valid, #GNUNET_SYSERR if not - */ -static int -verify_deposit_signature_conflict ( - const struct TALER_EXCHANGE_DepositHandle *dh, - const json_t *json) -{ - json_t *history; - struct TALER_Amount total; - enum TALER_ErrorCode ec; - struct GNUNET_HashCode h_denom_pub; - - memset (&h_denom_pub, - 0, - sizeof (h_denom_pub)); - history = json_object_get (json, - "history"); - if (GNUNET_OK != - TALER_EXCHANGE_verify_coin_history (&dh->dki, - dh->dki.value.currency, - &dh->depconf.coin_pub, - history, - &h_denom_pub, - &total)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - ec = TALER_JSON_get_error_code (json); - switch (ec) - { - case TALER_EC_EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS: - if (0 > - TALER_amount_add (&total, - &total, - &dh->amount_with_fee)) - { - /* clearly not OK if our transaction would have caused - the overflow... */ - return GNUNET_OK; - } - - if (0 >= TALER_amount_cmp (&total, - &dh->dki.value)) - { - /* transaction should have still fit */ - GNUNET_break (0); - return GNUNET_SYSERR; - } - /* everything OK, proof of double-spending was provided */ - return GNUNET_OK; - case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: - if (0 != GNUNET_memcmp (&dh->dki.h_key, - &h_denom_pub)) - return GNUNET_OK; /* indeed, proof with different denomination key provided */ - /* invalid proof provided */ - return GNUNET_SYSERR; - default: - /* unexpected error code */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } -} - - -/** - * Function called when we're done processing the - * HTTP /deposit request. - * - * @param cls the `struct TALER_EXCHANGE_DepositHandle` - * @param response_code HTTP response code, 0 on error - * @param response parsed JSON result, NULL on error - */ -static void -handle_deposit_finished (void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_DepositHandle *dh = cls; - struct TALER_ExchangeSignatureP exchange_sig; - struct TALER_ExchangePublicKeyP exchange_pub; - struct TALER_ExchangeSignatureP *es = NULL; - struct TALER_ExchangePublicKeyP *ep = NULL; - const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code - }; - - dh->job = NULL; - switch (response_code) - { - case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - if (GNUNET_OK != - verify_deposit_signature_ok (dh, - j, - &exchange_sig, - &exchange_pub)) - { - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE; - } - else - { - es = &exchange_sig; - ep = &exchange_pub; - } - 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: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); - /* 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 */ - break; - case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); - /* Nothing really to verify, this should never - happen, we should pass the JSON reply to the application */ - break; - case MHD_HTTP_CONFLICT: - /* Double spending; check signatures on transaction history */ - if (GNUNET_OK != - verify_deposit_signature_conflict (dh, - j)) - { - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE; - } - 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: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - break; - default: - /* unexpected response code */ - 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 deposit\n", - (unsigned int) response_code, - hr.ec); - GNUNET_break_op (0); - break; - } - dh->cb (dh->cb_cls, - &hr, - GNUNET_TIME_absolute_ntoh (dh->depconf.exchange_timestamp), - es, - ep); - TALER_EXCHANGE_deposit_cancel (dh); -} - - -/** - * Verify signature information about the deposit. - * - * @param dki public key information - * @param amount the amount to be deposited - * @param h_wire hash of the merchant’s account details - * @param h_contract_terms hash of the contact of the merchant with the customer (further details are never disclosed to the exchange) - * @param coin_pub coin’s public key - * @param denom_pub denomination key with which the coin is signed - * @param denom_pub_hash hash of @a denom_pub - * @param denom_sig exchange’s unblinded signature of the coin - * @param timestamp timestamp when the deposit was finalized - * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) - * @param refund_deadline date until which the merchant can issue a refund to the customer via the exchange (can be zero if refunds are not allowed) - * @param coin_sig the signature made with purpose #TALER_SIGNATURE_WALLET_COIN_DEPOSIT made by the customer with the coin’s private key. - * @return #GNUNET_OK if signatures are OK, #GNUNET_SYSERR if not - */ -static int -verify_signatures (const struct TALER_EXCHANGE_DenomPublicKey *dki, - const struct TALER_Amount *amount, - const struct GNUNET_HashCode *h_wire, - const struct GNUNET_HashCode *h_contract_terms, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_DenominationSignature *denom_sig, - const struct TALER_DenominationPublicKey *denom_pub, - const struct GNUNET_HashCode *denom_pub_hash, - struct GNUNET_TIME_Absolute timestamp, - const struct TALER_MerchantPublicKeyP *merchant_pub, - struct GNUNET_TIME_Absolute refund_deadline, - const struct TALER_CoinSpendSignatureP *coin_sig) -{ - { - struct TALER_DepositRequestPS dr = { - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_DEPOSIT), - .purpose.size = htonl (sizeof (dr)), - .h_contract_terms = *h_contract_terms, - .h_wire = *h_wire, - .h_denom_pub = *denom_pub_hash, - .wallet_timestamp = GNUNET_TIME_absolute_hton (timestamp), - .refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline), - .merchant = *merchant_pub, - .coin_pub = *coin_pub - }; - - TALER_amount_hton (&dr.amount_with_fee, - amount); - TALER_amount_hton (&dr.deposit_fee, - &dki->fee_deposit); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_DEPOSIT, - &dr, - &coin_sig->eddsa_signature, - &coin_pub->eddsa_pub)) - { - GNUNET_break_op (0); - TALER_LOG_WARNING ("Invalid coin signature on /deposit request!\n"); - { - TALER_LOG_DEBUG ("... amount_with_fee was %s\n", - TALER_amount2s (amount)); - TALER_LOG_DEBUG ("... deposit_fee was %s\n", - TALER_amount2s (&dki->fee_deposit)); - } - return GNUNET_SYSERR; - } - } - - /* check coin signature */ - { - struct TALER_CoinPublicInfo coin_info = { - .coin_pub = *coin_pub, - .denom_pub_hash = *denom_pub_hash, - .denom_sig = *denom_sig - }; - - if (GNUNET_YES != - TALER_test_coin_valid (&coin_info, - denom_pub)) - { - GNUNET_break_op (0); - TALER_LOG_WARNING ("Invalid coin passed for /deposit\n"); - return GNUNET_SYSERR; - } - } - - /* Check coin does make a contribution */ - if (0 < TALER_amount_cmp (&dki->fee_deposit, - amount)) - { - GNUNET_break_op (0); - TALER_LOG_WARNING ("Deposit amount smaller than fee\n"); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -void -TALER_EXCHANGE_deposit_permission_sign ( - const struct TALER_Amount *amount, - const struct TALER_Amount *deposit_fee, - const struct GNUNET_HashCode *h_wire, - const struct GNUNET_HashCode *h_contract_terms, - const struct GNUNET_HashCode *h_denom_pub, - const struct TALER_CoinSpendPrivateKeyP *coin_priv, - struct GNUNET_TIME_Absolute wallet_timestamp, - const struct TALER_MerchantPublicKeyP *merchant_pub, - struct GNUNET_TIME_Absolute refund_deadline, - struct TALER_CoinSpendSignatureP *coin_sig) -{ - struct TALER_DepositRequestPS dr = { - .purpose.size = htonl - (sizeof (dr)), - .purpose.purpose = htonl - (TALER_SIGNATURE_WALLET_COIN_DEPOSIT), - .h_contract_terms = *h_contract_terms, - .h_wire = *h_wire, - .h_denom_pub = *h_denom_pub, - .wallet_timestamp = GNUNET_TIME_absolute_hton (wallet_timestamp), - .refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline), - .merchant = *merchant_pub - }; - - GNUNET_assert (GNUNET_OK == - GNUNET_TIME_round_abs (&wallet_timestamp)); - GNUNET_assert (GNUNET_OK == - GNUNET_TIME_round_abs (&refund_deadline)); - GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv, - &dr.coin_pub.eddsa_pub); - TALER_amount_hton (&dr.amount_with_fee, - amount); - TALER_amount_hton (&dr.deposit_fee, - deposit_fee); - GNUNET_CRYPTO_eddsa_sign (&coin_priv->eddsa_priv, - &dr, - &coin_sig->eddsa_signature); -} - - -struct TALER_EXCHANGE_DepositHandle * -TALER_EXCHANGE_deposit (struct TALER_EXCHANGE_Handle *exchange, - const struct TALER_Amount *amount, - struct GNUNET_TIME_Absolute wire_deadline, - json_t *wire_details, - const struct GNUNET_HashCode *h_contract_terms, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_DenominationSignature *denom_sig, - const struct TALER_DenominationPublicKey *denom_pub, - struct GNUNET_TIME_Absolute timestamp, - const struct TALER_MerchantPublicKeyP *merchant_pub, - struct GNUNET_TIME_Absolute refund_deadline, - const struct TALER_CoinSpendSignatureP *coin_sig, - TALER_EXCHANGE_DepositResultCallback cb, - void *cb_cls, - enum TALER_ErrorCode *ec) -{ - const struct TALER_EXCHANGE_Keys *key_state; - const struct TALER_EXCHANGE_DenomPublicKey *dki; - struct TALER_EXCHANGE_DepositHandle *dh; - struct GNUNET_CURL_Context *ctx; - json_t *deposit_obj; - CURL *eh; - struct GNUNET_HashCode h_wire; - struct GNUNET_HashCode denom_pub_hash; - struct TALER_Amount amount_without_fee; - char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32]; - - { - char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; - char *end; - - end = GNUNET_STRINGS_data_to_string ( - coin_pub, - sizeof (struct TALER_CoinSpendPublicKeyP), - pub_str, - sizeof (pub_str)); - *end = '\0'; - GNUNET_snprintf (arg_str, - sizeof (arg_str), - "/coins/%s/deposit", - pub_str); - } - (void) GNUNET_TIME_round_abs (&wire_deadline); - (void) GNUNET_TIME_round_abs (&refund_deadline); - if (refund_deadline.abs_value_us > wire_deadline.abs_value_us) - { - GNUNET_break_op (0); - *ec = TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE; - return NULL; - } - GNUNET_assert (GNUNET_YES == - TEAH_handle_is_ready (exchange)); - /* initialize h_wire */ - if (GNUNET_OK != - TALER_JSON_merchant_wire_signature_hash (wire_details, - &h_wire)) - { - GNUNET_break (0); - *ec = TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH; - return NULL; - } - key_state = TALER_EXCHANGE_get_keys (exchange); - dki = TALER_EXCHANGE_get_denomination_key (key_state, - denom_pub); - if (NULL == dki) - { - *ec = TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; - GNUNET_break_op (0); - return NULL; - } - if (0 > - TALER_amount_subtract (&amount_without_fee, - amount, - &dki->fee_deposit)) - { - *ec = TALER_EC_EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT; - GNUNET_break_op (0); - return NULL; - } - GNUNET_CRYPTO_rsa_public_key_hash (denom_pub->rsa_public_key, - &denom_pub_hash); - if (GNUNET_OK != - verify_signatures (dki, - amount, - &h_wire, - h_contract_terms, - coin_pub, - denom_sig, - denom_pub, - &denom_pub_hash, - timestamp, - merchant_pub, - refund_deadline, - coin_sig)) - { - *ec = TALER_EC_EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID; - GNUNET_break_op (0); - return NULL; - } - - deposit_obj = GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("contribution", - amount), - GNUNET_JSON_pack_object_incref ("wire", - wire_details), - GNUNET_JSON_pack_data_auto ("h_wire", - &h_wire), - GNUNET_JSON_pack_data_auto ("h_contract_terms", - h_contract_terms), - GNUNET_JSON_pack_data_auto ("denom_pub_hash", - &denom_pub_hash), - TALER_JSON_pack_denomination_signature ("ub_sig", - denom_sig), - GNUNET_JSON_pack_time_abs ("timestamp", - timestamp), - GNUNET_JSON_pack_data_auto ("merchant_pub", - merchant_pub), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_time_abs ("refund_deadline", - refund_deadline)), - GNUNET_JSON_pack_time_abs ("wire_transfer_deadline", - wire_deadline), - GNUNET_JSON_pack_data_auto ("coin_sig", - coin_sig)); - dh = GNUNET_new (struct TALER_EXCHANGE_DepositHandle); - dh->auditor_chance = AUDITOR_CHANCE; - dh->exchange = exchange; - dh->cb = cb; - dh->cb_cls = cb_cls; - dh->url = TEAH_path_to_url (exchange, - arg_str); - if (NULL == dh->url) - { - GNUNET_break (0); - *ec = TALER_EC_GENERIC_ALLOCATION_FAILURE; - GNUNET_free (dh); - json_decref (deposit_obj); - return NULL; - } - dh->depconf.purpose.size = htonl (sizeof (struct - TALER_DepositConfirmationPS)); - dh->depconf.purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT); - dh->depconf.h_contract_terms = *h_contract_terms; - dh->depconf.h_wire = h_wire; - /* dh->depconf.exchange_timestamp; -- initialized later from exchange reply! */ - dh->depconf.refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline); - TALER_amount_hton (&dh->depconf.amount_without_fee, - &amount_without_fee); - dh->depconf.coin_pub = *coin_pub; - dh->depconf.merchant = *merchant_pub; - dh->amount_with_fee = *amount; - dh->dki = *dki; - dh->dki.key.rsa_public_key = NULL; /* lifetime not warranted, so better - not copy the pointer */ - - eh = TALER_EXCHANGE_curl_easy_get_ (dh->url); - if ( (NULL == eh) || - (GNUNET_OK != - TALER_curl_easy_post (&dh->ctx, - eh, - deposit_obj)) ) - { - *ec = TALER_EC_GENERIC_CURL_ALLOCATION_FAILURE; - GNUNET_break (0); - if (NULL != eh) - curl_easy_cleanup (eh); - json_decref (deposit_obj); - GNUNET_free (dh->url); - GNUNET_free (dh); - return NULL; - } - json_decref (deposit_obj); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "URL for deposit: `%s'\n", - dh->url); - ctx = TEAH_handle_to_context (exchange); - dh->job = GNUNET_CURL_job_add2 (ctx, - eh, - dh->ctx.headers, - &handle_deposit_finished, - dh); - return dh; -} - - -void -TALER_EXCHANGE_deposit_force_dc (struct TALER_EXCHANGE_DepositHandle *deposit) -{ - deposit->auditor_chance = 1; -} - - -void -TALER_EXCHANGE_deposit_cancel (struct TALER_EXCHANGE_DepositHandle *deposit) -{ - if (NULL != deposit->job) - { - GNUNET_CURL_job_cancel (deposit->job); - deposit->job = NULL; - } - GNUNET_free (deposit->url); - TALER_curl_easy_post_finished (&deposit->ctx); - GNUNET_free (deposit); -} - - -/* end of exchange_api_deposit.c */ diff --git a/src/lib/exchange_api_deposits_get.c b/src/lib/exchange_api_deposits_get.c index efe9070f1..20eaea3d3 100644 --- a/src/lib/exchange_api_deposits_get.c +++ b/src/lib/exchange_api_deposits_get.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2020 Taler Systems SA + Copyright (C) 2014-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 @@ -39,9 +39,9 @@ struct TALER_EXCHANGE_DepositGetHandle { /** - * The connection to exchange this request handle will use + * The keys of the this request handle will use */ - struct TALER_EXCHANGE_Handle *exchange; + struct TALER_EXCHANGE_Keys *keys; /** * The url for this request. @@ -70,52 +70,22 @@ struct TALER_EXCHANGE_DepositGetHandle void *cb_cls; /** - * Information the exchange should sign in response. - * (with pre-filled fields from the request). + * Hash over the wiring information of the merchant. */ - struct TALER_ConfirmWirePS depconf; - -}; + struct TALER_MerchantWireHashP h_wire; + /** + * Hash over the contract for which this deposit is made. + */ + struct TALER_PrivateContractHashP h_contract_terms; -/** - * Verify that the signature on the "200 OK" response - * from the exchange is valid. - * - * @param dwh deposit wtid handle - * @param json json reply with the signature - * @param exchange_pub the exchange's public key - * @param exchange_sig the exchange's signature - * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not - */ -static int -verify_deposit_wtid_signature_ok ( - const struct TALER_EXCHANGE_DepositGetHandle *dwh, - const json_t *json, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_ExchangeSignatureP *exchange_sig) -{ - const struct TALER_EXCHANGE_Keys *key_state; + /** + * The coin's public key. This is the value that must have been + * signed (blindly) by the Exchange. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; - key_state = TALER_EXCHANGE_get_keys (dwh->exchange); - if (GNUNET_OK != - TALER_EXCHANGE_test_signing_key (key_state, - exchange_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE, - &dwh->depconf, - &exchange_sig->eddsa_signature, - &exchange_pub->eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} +}; /** @@ -133,70 +103,91 @@ handle_deposit_wtid_finished (void *cls, { struct TALER_EXCHANGE_DepositGetHandle *dwh = cls; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_GetDepositResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; dwh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: { - struct TALER_EXCHANGE_DepositData dd; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("wtid", &dwh->depconf.wtid), - TALER_JSON_spec_absolute_time ("execution_time", &dd.execution_time), - TALER_JSON_spec_amount_any ("coin_contribution", &dd.coin_contribution), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", &dd.exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", &dd.exchange_pub), + GNUNET_JSON_spec_fixed_auto ("wtid", + &dr.details.ok.wtid), + GNUNET_JSON_spec_timestamp ("execution_time", + &dr.details.ok.execution_time), + TALER_JSON_spec_amount_any ("coin_contribution", + &dr.details.ok.coin_contribution), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &dr.details.ok.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &dr.details.ok.exchange_pub), GNUNET_JSON_spec_end () }; + const struct TALER_EXCHANGE_Keys *key_state; + key_state = dwh->keys; + GNUNET_assert (NULL != key_state); 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; + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } - dwh->depconf.execution_time = GNUNET_TIME_absolute_hton ( - dd.execution_time); - TALER_amount_hton (&dwh->depconf.coin_contribution, - &dd.coin_contribution); if (GNUNET_OK != - verify_deposit_wtid_signature_ok (dwh, - j, - &dd.exchange_pub, - &dd.exchange_sig)) + TALER_EXCHANGE_test_signing_key (key_state, + &dr.details.ok.exchange_pub)) { GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE; + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE; + break; } - else + if (GNUNET_OK != + TALER_exchange_online_confirm_wire_verify ( + &dwh->h_wire, + &dwh->h_contract_terms, + &dr.details.ok.wtid, + &dwh->coin_pub, + dr.details.ok.execution_time, + &dr.details.ok.coin_contribution, + &dr.details.ok.exchange_pub, + &dr.details.ok.exchange_sig)) { - dd.wtid = dwh->depconf.wtid; - dwh->cb (dwh->cb_cls, - &hr, - &dd); - TALER_EXCHANGE_deposits_get_cancel (dwh); - return; + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE; + break; } + dwh->cb (dwh->cb_cls, + &dr); + TALER_EXCHANGE_deposits_get_cancel (dwh); + return; } - break; case MHD_HTTP_ACCEPTED: { /* Transaction known, but not executed yet */ - struct GNUNET_TIME_Absolute execution_time; + bool no_legi = false; struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_absolute_time ("execution_time", &execution_time), + GNUNET_JSON_spec_timestamp ("execution_time", + &dr.details.accepted.execution_time), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("requirement_row", + &dr.details.accepted.requirement_row), + &no_legi), + TALER_JSON_spec_aml_decision ("aml_decision", + &dr.details.accepted.aml_decision), + GNUNET_JSON_spec_bool ("kyc_ok", + &dr.details.accepted.kyc_ok), GNUNET_JSON_spec_end () }; @@ -206,184 +197,167 @@ handle_deposit_wtid_finished (void *cls, NULL, NULL)) { GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } - else - { - struct TALER_EXCHANGE_DepositData dd = { - .execution_time = execution_time - }; - - dwh->cb (dwh->cb_cls, - &hr, - &dd); - TALER_EXCHANGE_deposits_get_cancel (dwh); - return; - } + if (no_legi) + dr.details.accepted.requirement_row = 0; + dwh->cb (dwh->cb_cls, + &dr); + TALER_EXCHANGE_deposits_get_cancel (dwh); + return; } - break; case MHD_HTTP_BAD_REQUEST: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); /* This should never happen, either us or the exchange is buggy (or API version conflict); just pass JSON reply to the application */ break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); /* 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 */ break; case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); /* Exchange does not know about transaction; we should pass the reply to the application */ break; case MHD_HTTP_INTERNAL_SERVER_ERROR: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); /* Server had an internal issue; we should retry, but this API leaves this to the application */ break; default: /* unexpected response code */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange GET deposits\n", (unsigned int) response_code, - (int) hr.ec); + (int) dr.hr.ec); GNUNET_break_op (0); break; } dwh->cb (dwh->cb_cls, - &hr, - NULL); + &dr); TALER_EXCHANGE_deposits_get_cancel (dwh); } -/** - * Obtain wire transfer details about an existing deposit operation. - * - * @param exchange the exchange to query - * @param merchant_priv the merchant's private key - * @param h_wire hash of merchant's wire transfer details - * @param h_contract_terms hash of the proposal data from the contract - * between merchant and customer - * @param coin_pub public key of the coin - * @param cb function to call with the result - * @param cb_cls closure for @a cb - * @return handle to abort request - */ struct TALER_EXCHANGE_DepositGetHandle * TALER_EXCHANGE_deposits_get ( - struct TALER_EXCHANGE_Handle *exchange, + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, const struct TALER_MerchantPrivateKeyP *merchant_priv, - const struct GNUNET_HashCode *h_wire, - const struct GNUNET_HashCode *h_contract_terms, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_PrivateContractHashP *h_contract_terms, const struct TALER_CoinSpendPublicKeyP *coin_pub, + struct GNUNET_TIME_Relative timeout, TALER_EXCHANGE_DepositGetCallback cb, void *cb_cls) { - struct TALER_DepositTrackPS dtp; + struct TALER_MerchantPublicKeyP merchant; struct TALER_MerchantSignatureP merchant_sig; struct TALER_EXCHANGE_DepositGetHandle *dwh; - struct GNUNET_CURL_Context *ctx; CURL *eh; char arg_str[(sizeof (struct TALER_CoinSpendPublicKeyP) - + sizeof (struct GNUNET_HashCode) + + sizeof (struct TALER_MerchantWireHashP) + sizeof (struct TALER_MerchantPublicKeyP) - + sizeof (struct GNUNET_HashCode) + + sizeof (struct TALER_PrivateContractHashP) + sizeof (struct TALER_MerchantSignatureP)) * 2 + 48]; + unsigned int tms + = (unsigned int) timeout.rel_value_us + / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us; - if (GNUNET_YES != - TEAH_handle_is_ready (exchange)) - { - GNUNET_break (0); - return NULL; - } - dtp.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_TRACK_TRANSACTION); - dtp.purpose.size = htonl (sizeof (dtp)); - dtp.h_contract_terms = *h_contract_terms; - dtp.h_wire = *h_wire; GNUNET_CRYPTO_eddsa_key_get_public (&merchant_priv->eddsa_priv, - &dtp.merchant.eddsa_pub); - - dtp.coin_pub = *coin_pub; - GNUNET_CRYPTO_eddsa_sign (&merchant_priv->eddsa_priv, - &dtp, - &merchant_sig.eddsa_sig); + &merchant.eddsa_pub); + TALER_merchant_deposit_sign (h_contract_terms, + h_wire, + coin_pub, + merchant_priv, + &merchant_sig); { char cpub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; char mpub_str[sizeof (struct TALER_MerchantPublicKeyP) * 2]; char msig_str[sizeof (struct TALER_MerchantSignatureP) * 2]; - char chash_str[sizeof (struct GNUNET_HashCode) * 2]; - char whash_str[sizeof (struct GNUNET_HashCode) * 2]; + char chash_str[sizeof (struct TALER_PrivateContractHashP) * 2]; + char whash_str[sizeof (struct TALER_MerchantWireHashP) * 2]; + char timeout_str[24]; char *end; end = GNUNET_STRINGS_data_to_string (h_wire, - sizeof (struct - GNUNET_HashCode), + sizeof (*h_wire), whash_str, sizeof (whash_str)); *end = '\0'; - end = GNUNET_STRINGS_data_to_string (&dtp.merchant, - sizeof (struct - TALER_MerchantPublicKeyP), + end = GNUNET_STRINGS_data_to_string (&merchant, + sizeof (merchant), mpub_str, sizeof (mpub_str)); *end = '\0'; end = GNUNET_STRINGS_data_to_string (h_contract_terms, - sizeof (struct - GNUNET_HashCode), + sizeof (*h_contract_terms), chash_str, sizeof (chash_str)); *end = '\0'; end = GNUNET_STRINGS_data_to_string (coin_pub, - sizeof (struct - TALER_CoinSpendPublicKeyP), + sizeof (*coin_pub), cpub_str, sizeof (cpub_str)); *end = '\0'; end = GNUNET_STRINGS_data_to_string (&merchant_sig, - sizeof (struct - TALER_MerchantSignatureP), + sizeof (merchant_sig), msig_str, sizeof (msig_str)); *end = '\0'; + if (GNUNET_TIME_relative_is_zero (timeout)) + { + timeout_str[0] = '\0'; + } + else + { + GNUNET_snprintf ( + timeout_str, + sizeof (timeout_str), + "%u", + tms); + } GNUNET_snprintf (arg_str, sizeof (arg_str), - "/deposits/%s/%s/%s/%s?merchant_sig=%s", + "deposits/%s/%s/%s/%s?merchant_sig=%s%s%s", whash_str, mpub_str, chash_str, cpub_str, - msig_str); + msig_str, + 0 == tms + ? "" + : "&timeout_ms=", + timeout_str); } dwh = GNUNET_new (struct TALER_EXCHANGE_DepositGetHandle); - dwh->exchange = exchange; dwh->cb = cb; dwh->cb_cls = cb_cls; - dwh->url = TEAH_path_to_url (exchange, - arg_str); + dwh->url = TALER_url_join (url, + arg_str, + NULL); if (NULL == dwh->url) { GNUNET_free (dwh); return NULL; } - dwh->depconf.purpose.size = htonl (sizeof (struct TALER_ConfirmWirePS)); - dwh->depconf.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE); - dwh->depconf.h_wire = *h_wire; - dwh->depconf.h_contract_terms = *h_contract_terms; - dwh->depconf.coin_pub = *coin_pub; - + dwh->h_wire = *h_wire; + dwh->h_contract_terms = *h_contract_terms; + dwh->coin_pub = *coin_pub; eh = TALER_EXCHANGE_curl_easy_get_ (dwh->url); if (NULL == eh) { @@ -392,21 +366,22 @@ TALER_EXCHANGE_deposits_get ( GNUNET_free (dwh); return NULL; } - ctx = TEAH_handle_to_context (exchange); + if (0 != tms) + { + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT_MS, + (long) (tms + 100L))); + } dwh->job = GNUNET_CURL_job_add (ctx, eh, &handle_deposit_wtid_finished, dwh); + dwh->keys = TALER_EXCHANGE_keys_incref (keys); return dwh; } -/** - * Cancel /deposits/$WTID request. This function cannot be used on a request - * handle if a response is already served for it. - * - * @param dwh the wire deposits request handle - */ void TALER_EXCHANGE_deposits_get_cancel (struct TALER_EXCHANGE_DepositGetHandle *dwh) { @@ -417,6 +392,7 @@ TALER_EXCHANGE_deposits_get_cancel (struct TALER_EXCHANGE_DepositGetHandle *dwh) } GNUNET_free (dwh->url); TALER_curl_easy_post_finished (&dwh->ctx); + TALER_EXCHANGE_keys_decref (dwh->keys); GNUNET_free (dwh); } diff --git a/src/lib/exchange_api_handle.c b/src/lib/exchange_api_handle.c index dfd5a3dc6..fdadc8d2a 100644 --- a/src/lib/exchange_api_handle.c +++ b/src/lib/exchange_api_handle.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2021 Taler Systems SA + Copyright (C) 2014-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 @@ -30,6 +30,7 @@ #include "taler_exchange_service.h" #include "taler_auditor_service.h" #include "taler_signatures.h" +#include "taler_extensions.h" #include "exchange_api_handle.h" #include "exchange_api_curl_defaults.h" #include "backoff.h" @@ -39,12 +40,17 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define EXCHANGE_PROTOCOL_CURRENT 9 +#define EXCHANGE_PROTOCOL_CURRENT 19 /** * How many versions are we backwards compatible with? */ -#define EXCHANGE_PROTOCOL_AGE 0 +#define EXCHANGE_PROTOCOL_AGE 2 + +/** + * Set to 1 for extra debug logging. + */ +#define DEBUG 0 /** * Current version for (local) JSON serialization of persisted @@ -53,191 +59,322 @@ #define EXCHANGE_SERIALIZATION_FORMAT_VERSION 0 /** - * Set to 1 for extra debug logging. + * How far off do we allow key lifetimes to be? */ -#define DEBUG 0 +#define LIFETIME_TOLERANCE GNUNET_TIME_UNIT_HOURS /** - * Log error related to CURL operations. - * - * @param type log level - * @param function which function failed to run - * @param code what was the curl error code + * If the "Expire" cache control header is missing, for + * how long do we assume the reply to be valid at least? */ -#define CURL_STRERROR(type, function, code) \ - GNUNET_log (type, "Curl function `%s' has failed at `%s:%d' with error: %s", \ - function, __FILE__, __LINE__, curl_easy_strerror (code)); - +#define DEFAULT_EXPIRATION GNUNET_TIME_UNIT_HOURS /** - * Data for the request to get the /keys of a exchange. + * If the "Expire" cache control header is missing, for + * how long do we assume the reply to be valid at least? */ -struct KeysRequest; +#define MINIMUM_EXPIRATION GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 2) /** - * Entry in DLL of auditors used by an exchange. + * Handle for a GET /keys request. */ -struct TEAH_AuditorListEntry +struct TALER_EXCHANGE_GetKeysHandle { - /** - * Next pointer of DLL. - */ - struct TEAH_AuditorListEntry *next; /** - * Prev pointer of DLL. + * The exchange base URL (i.e. "https://exchange.demo.taler.net/") */ - struct TEAH_AuditorListEntry *prev; + char *exchange_url; /** - * Base URL of the auditor. + * The url for the /keys request. */ - char *auditor_url; + char *url; /** - * Handle to the auditor. + * Previous /keys response, NULL for none. */ - struct TALER_AUDITOR_Handle *ah; + struct TALER_EXCHANGE_Keys *prev_keys; /** - * Head of DLL of interactions with this auditor. + * Entry for this request with the `struct GNUNET_CURL_Context`. */ - struct TEAH_AuditorInteractionEntry *ai_head; + struct GNUNET_CURL_Job *job; /** - * Tail of DLL of interactions with this auditor. + * Expiration time according to "Expire:" header. + * 0 if not provided by the server. */ - struct TEAH_AuditorInteractionEntry *ai_tail; + struct GNUNET_TIME_Timestamp expire; /** - * Public key of the auditor. + * Function to call with the exchange's certification data, + * NULL if this has already been done. */ - struct TALER_AuditorPublicKeyP auditor_pub; + TALER_EXCHANGE_GetKeysCallback cert_cb; /** - * Flag indicating that the auditor is available and that protocol - * version compatibility is given. + * Closure to pass to @e cert_cb. */ - int is_up; + void *cert_cb_cls; }; -/* ***************** Internal /keys fetching ************* */ - /** - * Data for the request to get the /keys of a exchange. + * Element in the `struct SignatureContext` array. */ -struct KeysRequest +struct SignatureElement { + /** - * The connection to exchange this request handle will use + * Offset of the denomination in the group array, + * for sorting (2nd rank, ascending). */ - struct TALER_EXCHANGE_Handle *exchange; + unsigned int offset; /** - * The url for this handle + * Offset of the group in the denominations array, + * for sorting (2nd rank, ascending). */ - char *url; + unsigned int group_offset; /** - * Entry for this request with the `struct GNUNET_CURL_Context`. + * Pointer to actual master signature to hash over. */ - struct GNUNET_CURL_Job *job; + struct TALER_MasterSignatureP master_sig; +}; +/** + * Context for collecting the array of master signatures + * needed to verify the exchange_sig online signature. + */ +struct SignatureContext +{ /** - * Expiration time according to "Expire:" header. - * 0 if not provided by the server. + * Array of signatures to hash over. + */ + struct SignatureElement *elements; + + /** + * Write offset in the @e elements array. */ - struct GNUNET_TIME_Absolute expire; + unsigned int elements_pos; + /** + * Allocated space for @e elements. + */ + unsigned int elements_size; }; /** - * Signature of functions called with the result from our call to the - * auditor's /deposit-confirmation handler. + * Determine order to sort two elements by before + * we hash the master signatures. Used for + * sorting with qsort(). * - * @param cls closure of type `struct TEAH_AuditorInteractionEntry *` - * @param hr HTTP response + * @param a pointer to a `struct SignatureElement` + * @param b pointer to a `struct SignatureElement` + * @return 0 if equal, -1 if a < b, 1 if a > b. */ -void -TEAH_acc_confirmation_cb (void *cls, - const struct TALER_AUDITOR_HttpResponse *hr) +static int +signature_context_sort_cb (const void *a, + const void *b) { - struct TEAH_AuditorInteractionEntry *aie = cls; - struct TEAH_AuditorListEntry *ale = aie->ale; + const struct SignatureElement *sa = a; + const struct SignatureElement *sb = b; - if (MHD_HTTP_OK != hr->http_status) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to submit deposit confirmation to auditor `%s' with HTTP status %d (EC: %d). This is acceptable if it does not happen often.\n", - ale->auditor_url, - hr->http_status, - hr->ec); - } - GNUNET_CONTAINER_DLL_remove (ale->ai_head, - ale->ai_tail, - aie); - GNUNET_free (aie); + if (sa->group_offset < sb->group_offset) + return -1; + if (sa->group_offset > sb->group_offset) + return 1; + if (sa->offset < sb->offset) + return -1; + if (sa->offset > sb->offset) + return 1; + /* We should never have two disjoint elements + with same time and offset */ + GNUNET_assert (sa == sb); + return 0; } /** - * Iterate over all available auditors for @a h, calling - * @a ac and giving it a chance to start a deposit - * confirmation interaction. + * Append a @a master_sig to the @a sig_ctx using the + * given attributes for (later) sorting. * - * @param h exchange to go over auditors for - * @param ac function to call per auditor - * @param ac_cls closure for @a ac + * @param[in,out] sig_ctx signature context to update + * @param group_offset offset for the group + * @param offset offset for the entry + * @param master_sig master signature for the entry */ -void -TEAH_get_auditors_for_dc (struct TALER_EXCHANGE_Handle *h, - TEAH_AuditorCallback ac, - void *ac_cls) +static void +append_signature (struct SignatureContext *sig_ctx, + unsigned int group_offset, + unsigned int offset, + const struct TALER_MasterSignatureP *master_sig) { - if (NULL == h->auditors_head) + struct SignatureElement *element; + unsigned int new_size; + + if (sig_ctx->elements_pos == sig_ctx->elements_size) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No auditor available for exchange `%s'. Not submitting deposit confirmations.\n", - h->url); - return; + if (0 == sig_ctx->elements_size) + new_size = 1024; + else + new_size = sig_ctx->elements_size * 2; + GNUNET_array_grow (sig_ctx->elements, + sig_ctx->elements_size, + new_size); } - for (struct TEAH_AuditorListEntry *ale = h->auditors_head; - NULL != ale; - ale = ale->next) + element = &sig_ctx->elements[sig_ctx->elements_pos++]; + element->offset = offset; + element->group_offset = group_offset; + element->master_sig = *master_sig; +} + + +/** + * Frees @a wfm array. + * + * @param wfm fee array to release + * @param wfm_len length of the @a wfm array + */ +static void +free_fees (struct TALER_EXCHANGE_WireFeesByMethod *wfm, + unsigned int wfm_len) +{ + for (unsigned int i = 0; i<wfm_len; i++) { - struct TEAH_AuditorInteractionEntry *aie; + struct TALER_EXCHANGE_WireFeesByMethod *wfmi = &wfm[i]; - if (GNUNET_NO == ale->is_up) - continue; - aie = ac (ac_cls, - ale->ah, - &ale->auditor_pub); - if (NULL != aie) + while (NULL != wfmi->fees_head) { - aie->ale = ale; - GNUNET_CONTAINER_DLL_insert (ale->ai_head, - ale->ai_tail, - aie); + struct TALER_EXCHANGE_WireAggregateFees *fe + = wfmi->fees_head; + + wfmi->fees_head = fe->next; + GNUNET_free (fe); } + GNUNET_free (wfmi->method); } + GNUNET_free (wfm); } /** - * Release memory occupied by a keys request. Note that this does not - * cancel the request itself. + * Parse wire @a fees and return array. * - * @param kr request to free + * @param master_pub master public key to use to check signatures + * @param currency currency amounts are expected in + * @param fees json AggregateTransferFee to parse + * @param[out] fees_len set to length of returned array + * @return NULL on error */ -static void -free_keys_request (struct KeysRequest *kr) +static struct TALER_EXCHANGE_WireFeesByMethod * +parse_fees (const struct TALER_MasterPublicKeyP *master_pub, + const char *currency, + const json_t *fees, + unsigned int *fees_len) { - GNUNET_free (kr->url); - GNUNET_free (kr); + struct TALER_EXCHANGE_WireFeesByMethod *fbm; + size_t fbml = json_object_size (fees); + unsigned int i = 0; + const char *key; + const json_t *fee_array; + + if (UINT_MAX < fbml) + { + GNUNET_break (0); + return NULL; + } + fbm = GNUNET_new_array (fbml, + struct TALER_EXCHANGE_WireFeesByMethod); + *fees_len = (unsigned int) fbml; + json_object_foreach ((json_t *) fees, key, fee_array) { + struct TALER_EXCHANGE_WireFeesByMethod *fe = &fbm[i++]; + size_t idx; + json_t *fee; + + fe->method = GNUNET_strdup (key); + fe->fees_head = NULL; + json_array_foreach (fee_array, idx, fee) + { + struct TALER_EXCHANGE_WireAggregateFees *wa + = GNUNET_new (struct TALER_EXCHANGE_WireAggregateFees); + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("sig", + &wa->master_sig), + TALER_JSON_spec_amount ("wire_fee", + currency, + &wa->fees.wire), + TALER_JSON_spec_amount ("closing_fee", + currency, + &wa->fees.closing), + GNUNET_JSON_spec_timestamp ("start_date", + &wa->start_date), + GNUNET_JSON_spec_timestamp ("end_date", + &wa->end_date), + GNUNET_JSON_spec_end () + }; + + wa->next = fe->fees_head; + fe->fees_head = wa; + if (GNUNET_OK != + GNUNET_JSON_parse (fee, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + free_fees (fbm, + i); + return NULL; + } + if (GNUNET_OK != + TALER_exchange_offline_wire_fee_verify ( + key, + wa->start_date, + wa->end_date, + &wa->fees, + master_pub, + &wa->master_sig)) + { + GNUNET_break_op (0); + free_fees (fbm, + i); + return NULL; + } + } /* for all fees over time */ + } /* for all methods */ + GNUNET_assert (i == fbml); + return fbm; +} + + +void +TEAH_get_auditors_for_dc ( + struct TALER_EXCHANGE_Keys *keys, + TEAH_AuditorCallback ac, + void *ac_cls) +{ + if (0 == keys->num_auditors) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No auditor available. Not submitting deposit confirmations.\n"); + return; + } + for (unsigned int i = 0; i<keys->num_auditors; i++) + { + const struct TALER_EXCHANGE_AuditorInformation *auditor + = &keys->auditors[i]; + + ac (ac_cls, + auditor->auditor_url, + &auditor->auditor_pub); + } } @@ -252,29 +389,28 @@ free_keys_request (struct KeysRequest *kr) * * @param[out] sign_key where to return the result * @param check_sigs should we check signatures? - * @param[in] sign_key_obj json to parse + * @param sign_key_obj json to parse * @param master_key master key to use to verify signature * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if the signature is - * invalid or the json malformed. + * invalid or the @a sign_key_obj is malformed. */ -static int +static enum GNUNET_GenericReturnValue parse_json_signkey (struct TALER_EXCHANGE_SigningPublicKey *sign_key, - int check_sigs, - json_t *sign_key_obj, + bool check_sigs, + const json_t *sign_key_obj, const struct TALER_MasterPublicKeyP *master_key) { - struct TALER_MasterSignatureP sign_key_issue_sig; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("master_sig", - &sign_key_issue_sig), + &sign_key->master_sig), GNUNET_JSON_spec_fixed_auto ("key", &sign_key->key), - TALER_JSON_spec_absolute_time ("stamp_start", - &sign_key->valid_from), - TALER_JSON_spec_absolute_time ("stamp_expire", - &sign_key->valid_until), - TALER_JSON_spec_absolute_time ("stamp_end", - &sign_key->valid_legal), + GNUNET_JSON_spec_timestamp ("stamp_start", + &sign_key->valid_from), + GNUNET_JSON_spec_timestamp ("stamp_expire", + &sign_key->valid_until), + GNUNET_JSON_spec_timestamp ("stamp_end", + &sign_key->valid_legal), GNUNET_JSON_spec_end () }; @@ -286,7 +422,6 @@ parse_json_signkey (struct TALER_EXCHANGE_SigningPublicKey *sign_key, GNUNET_break_op (0); return GNUNET_SYSERR; } - if (! check_sigs) return GNUNET_OK; if (GNUNET_OK != @@ -296,57 +431,64 @@ parse_json_signkey (struct TALER_EXCHANGE_SigningPublicKey *sign_key, sign_key->valid_until, sign_key->valid_legal, master_key, - &sign_key_issue_sig)) + &sign_key->master_sig)) { GNUNET_break_op (0); return GNUNET_SYSERR; } - sign_key->master_sig = sign_key_issue_sig; return GNUNET_OK; } /** - * Parse a exchange's denomination key encoded in JSON. + * Parse a exchange's denomination key encoded in JSON partially. + * + * Only the values for master_sig, timestamps and the cipher-specific public + * key are parsed. All other fields (fees, age_mask, value) MUST have been set + * prior to calling this function, otherwise the signature verification + * performed within this function will fail. * * @param[out] denom_key where to return the result + * @param cipher cipher type to parse * @param check_sigs should we check signatures? - * @param[in] denom_key_obj json to parse + * @param denom_key_obj json to parse * @param master_key master key to use to verify signature - * @param hash_context where to accumulate data for signature verification + * @param group_offset offset for the group + * @param index index of this denomination key in the group + * @param sig_ctx where to write details about encountered + * master signatures, NULL if not used * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if the signature is * invalid or the json malformed. */ -static int -parse_json_denomkey (struct TALER_EXCHANGE_DenomPublicKey *denom_key, - int check_sigs, - json_t *denom_key_obj, - struct TALER_MasterPublicKeyP *master_key, - struct GNUNET_HashContext *hash_context) +static enum GNUNET_GenericReturnValue +parse_json_denomkey_partially ( + struct TALER_EXCHANGE_DenomPublicKey *denom_key, + enum GNUNET_CRYPTO_BlindSignatureAlgorithm cipher, + bool check_sigs, + const json_t *denom_key_obj, + struct TALER_MasterPublicKeyP *master_key, + unsigned int group_offset, + unsigned int index, + struct SignatureContext *sig_ctx) { struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("master_sig", &denom_key->master_sig), - TALER_JSON_spec_absolute_time ("stamp_expire_deposit", - &denom_key->expire_deposit), - TALER_JSON_spec_absolute_time ("stamp_expire_withdraw", - &denom_key->withdraw_valid_until), - TALER_JSON_spec_absolute_time ("stamp_start", - &denom_key->valid_from), - TALER_JSON_spec_absolute_time ("stamp_expire_legal", - &denom_key->expire_legal), - TALER_JSON_spec_amount_any ("value", - &denom_key->value), - TALER_JSON_spec_amount_any ("fee_withdraw", - &denom_key->fee_withdraw), - TALER_JSON_spec_amount_any ("fee_deposit", - &denom_key->fee_deposit), - TALER_JSON_spec_amount_any ("fee_refresh", - &denom_key->fee_refresh), - TALER_JSON_spec_amount_any ("fee_refund", - &denom_key->fee_refund), - GNUNET_JSON_spec_rsa_public_key ("denom_pub", - &denom_key->key.rsa_public_key), + GNUNET_JSON_spec_timestamp ("stamp_expire_deposit", + &denom_key->expire_deposit), + GNUNET_JSON_spec_timestamp ("stamp_expire_withdraw", + &denom_key->withdraw_valid_until), + GNUNET_JSON_spec_timestamp ("stamp_start", + &denom_key->valid_from), + GNUNET_JSON_spec_timestamp ("stamp_expire_legal", + &denom_key->expire_legal), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("lost", + &denom_key->lost), + NULL), + TALER_JSON_spec_denom_pub_cipher (NULL, + cipher, + &denom_key->key), GNUNET_JSON_spec_end () }; @@ -358,13 +500,13 @@ parse_json_denomkey (struct TALER_EXCHANGE_DenomPublicKey *denom_key, GNUNET_break_op (0); return GNUNET_SYSERR; } - - GNUNET_CRYPTO_rsa_public_key_hash (denom_key->key.rsa_public_key, - &denom_key->h_key); - if (NULL != hash_context) - GNUNET_CRYPTO_hash_context_read (hash_context, - &denom_key->h_key, - sizeof (struct GNUNET_HashCode)); + TALER_denom_pub_hash (&denom_key->key, + &denom_key->h_key); + if (NULL != sig_ctx) + append_signature (sig_ctx, + group_offset, + index, + &denom_key->master_sig); if (! check_sigs) return GNUNET_OK; EXITIF (GNUNET_SYSERR == @@ -375,19 +517,16 @@ parse_json_denomkey (struct TALER_EXCHANGE_DenomPublicKey *denom_key, denom_key->expire_deposit, denom_key->expire_legal, &denom_key->value, - &denom_key->fee_withdraw, - &denom_key->fee_deposit, - &denom_key->fee_refresh, - &denom_key->fee_refund, + &denom_key->fees, master_key, &denom_key->master_sig)); return GNUNET_OK; EXITIF_exit: + GNUNET_JSON_parse_free (spec); /* invalidate denom_key, just to be sure */ memset (denom_key, 0, sizeof (*denom_key)); - GNUNET_JSON_parse_free (spec); return GNUNET_SYSERR; } @@ -397,30 +536,29 @@ EXITIF_exit: * * @param[out] auditor where to return the result * @param check_sigs should we check signatures - * @param[in] auditor_obj json to parse + * @param auditor_obj json to parse * @param key_data information about denomination keys * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if the signature is * invalid or the json malformed. */ -static int +static enum GNUNET_GenericReturnValue parse_json_auditor (struct TALER_EXCHANGE_AuditorInformation *auditor, - int check_sigs, - json_t *auditor_obj, + bool check_sigs, + const json_t *auditor_obj, const struct TALER_EXCHANGE_Keys *key_data) { - json_t *keys; + const json_t *keys; json_t *key; - unsigned int len; - unsigned int off; - unsigned int i; + size_t off; + size_t pos; const char *auditor_url; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("auditor_pub", &auditor->auditor_pub), - GNUNET_JSON_spec_string ("auditor_url", + TALER_JSON_spec_web_url ("auditor_url", &auditor_url), - GNUNET_JSON_spec_json ("denomination_keys", - &keys), + GNUNET_JSON_spec_array_const ("denomination_keys", + &keys), GNUNET_JSON_spec_end () }; @@ -438,16 +576,15 @@ parse_json_auditor (struct TALER_EXCHANGE_AuditorInformation *auditor, return GNUNET_SYSERR; } auditor->auditor_url = GNUNET_strdup (auditor_url); - len = json_array_size (keys); - auditor->denom_keys = GNUNET_new_array (len, - struct - TALER_EXCHANGE_AuditorDenominationInfo); - off = 0; - json_array_foreach (keys, i, key) { + auditor->denom_keys + = GNUNET_new_array (json_array_size (keys), + struct TALER_EXCHANGE_AuditorDenominationInfo); + pos = 0; + json_array_foreach (keys, off, key) { struct TALER_AuditorSignatureP auditor_sig; - struct GNUNET_HashCode denom_h; - const struct TALER_EXCHANGE_DenomPublicKey *dk; - unsigned int dk_off; + struct TALER_DenominationHashP denom_h; + const struct TALER_EXCHANGE_DenomPublicKey *dk = NULL; + unsigned int dk_off = UINT_MAX; struct GNUNET_JSON_Specification kspec[] = { GNUNET_JSON_spec_fixed_auto ("auditor_sig", &auditor_sig), @@ -464,8 +601,6 @@ parse_json_auditor (struct TALER_EXCHANGE_AuditorInformation *auditor, GNUNET_break_op (0); continue; } - dk = NULL; - dk_off = UINT_MAX; for (unsigned int j = 0; j<key_data->num_denom_keys; j++) { if (0 == GNUNET_memcmp (&denom_h, @@ -480,7 +615,7 @@ parse_json_auditor (struct TALER_EXCHANGE_AuditorInformation *auditor, { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Auditor signed denomination %s, which we do not know. Ignoring signature.\n", - GNUNET_h2s (&denom_h)); + GNUNET_h2s (&denom_h.hash)); continue; } if (check_sigs) @@ -495,125 +630,95 @@ parse_json_auditor (struct TALER_EXCHANGE_AuditorInformation *auditor, dk->expire_deposit, dk->expire_legal, &dk->value, - &dk->fee_withdraw, - &dk->fee_deposit, - &dk->fee_refresh, - &dk->fee_refund, + &dk->fees, &auditor->auditor_pub, &auditor_sig)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); return GNUNET_SYSERR; } } - auditor->denom_keys[off].denom_key_offset = dk_off; - auditor->denom_keys[off].auditor_sig = auditor_sig; - off++; + auditor->denom_keys[pos].denom_key_offset = dk_off; + auditor->denom_keys[pos].auditor_sig = auditor_sig; + pos++; } - auditor->num_denom_keys = off; - GNUNET_JSON_parse_free (spec); + if (pos > UINT_MAX) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + auditor->num_denom_keys = (unsigned int) pos; return GNUNET_OK; } /** - * Function called with information about the auditor. Marks an - * auditor as 'up'. + * Parse a exchange's global fee information encoded in JSON. * - * @param cls closure, a `struct TEAH_AuditorListEntry *` - * @param hr http response from the auditor - * @param vi basic information about the auditor - * @param compat protocol compatibility information + * @param[out] gf where to return the result + * @param check_sigs should we check signatures + * @param fee_obj json to parse + * @param key_data already parsed information about the exchange + * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if the signature is + * invalid or the json malformed. */ -static void -auditor_version_cb ( - void *cls, - const struct TALER_AUDITOR_HttpResponse *hr, - const struct TALER_AUDITOR_VersionInformation *vi, - enum TALER_AUDITOR_VersionCompatibility compat) +static enum GNUNET_GenericReturnValue +parse_global_fee (struct TALER_EXCHANGE_GlobalFee *gf, + bool check_sigs, + const json_t *fee_obj, + const struct TALER_EXCHANGE_Keys *key_data) { - struct TEAH_AuditorListEntry *ale = cls; - - if (NULL == vi) - { - /* In this case, we don't mark the auditor as 'up' */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Auditor `%s' gave unexpected version response.\n", - ale->auditor_url); - return; - } + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("start_date", + &gf->start_date), + GNUNET_JSON_spec_timestamp ("end_date", + &gf->end_date), + GNUNET_JSON_spec_relative_time ("purse_timeout", + &gf->purse_timeout), + GNUNET_JSON_spec_relative_time ("history_expiration", + &gf->history_expiration), + GNUNET_JSON_spec_uint32 ("purse_account_limit", + &gf->purse_account_limit), + TALER_JSON_SPEC_GLOBAL_FEES (key_data->currency, + &gf->fees), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &gf->master_sig), + GNUNET_JSON_spec_end () + }; - if (0 != (TALER_AUDITOR_VC_INCOMPATIBLE & compat)) + if (GNUNET_OK != + GNUNET_JSON_parse (fee_obj, + spec, + NULL, NULL)) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Auditor `%s' runs incompatible protocol version!\n", - ale->auditor_url); - if (0 != (TALER_AUDITOR_VC_OLDER & compat)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Auditor `%s' runs outdated protocol version!\n", - ale->auditor_url); - } - if (0 != (TALER_AUDITOR_VC_NEWER & compat)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Auditor `%s' runs more recent incompatible version. We should upgrade!\n", - ale->auditor_url); - } - return; + GNUNET_break_op (0); +#if DEBUG + json_dumpf (fee_obj, + stderr, + JSON_INDENT (2)); +#endif + return GNUNET_SYSERR; } - ale->is_up = GNUNET_YES; -} - - -/** - * Recalculate our auditor list, we got /keys and it may have - * changed. - * - * @param exchange exchange for which to update the list. - */ -static void -update_auditors (struct TALER_EXCHANGE_Handle *exchange) -{ - struct TALER_EXCHANGE_Keys *kd = &exchange->key_data; - - TALER_LOG_DEBUG ("Updating auditors\n"); - for (unsigned int i = 0; i<kd->num_auditors; i++) + if (check_sigs) { - /* Compare auditor data from /keys with auditor data - * from owned exchange structures. */ - struct TALER_EXCHANGE_AuditorInformation *auditor = &kd->auditors[i]; - struct TEAH_AuditorListEntry *ale = NULL; - - for (struct TEAH_AuditorListEntry *a = exchange->auditors_head; - NULL != a; - a = a->next) + if (GNUNET_OK != + TALER_exchange_offline_global_fee_verify ( + gf->start_date, + gf->end_date, + &gf->fees, + gf->purse_timeout, + gf->history_expiration, + gf->purse_account_limit, + &key_data->master_pub, + &gf->master_sig)) { - if (0 == GNUNET_memcmp (&auditor->auditor_pub, - &a->auditor_pub)) - { - ale = a; - break; - } + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; } - if (NULL != ale) - continue; /* found, no need to add */ - - /* new auditor, add */ - TALER_LOG_DEBUG ("Found new auditor!\n"); - ale = GNUNET_new (struct TEAH_AuditorListEntry); - ale->auditor_pub = auditor->auditor_pub; - ale->auditor_url = GNUNET_strdup (auditor->auditor_url); - GNUNET_CONTAINER_DLL_insert (exchange->auditors_head, - exchange->auditors_tail, - ale); - - ale->ah = TALER_AUDITOR_connect (exchange->ctx, - ale->auditor_url, - &auditor_version_cb, - ale); } + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; } @@ -623,39 +728,31 @@ update_auditors (struct TALER_EXCHANGE_Handle *exchange) * @param denom1 first denomination key * @param denom2 second denomination key * @return 0 if the two keys are equal (not necessarily - * the same object), 1 otherwise. + * the same object), non-zero otherwise. */ static unsigned int -denoms_cmp (struct TALER_EXCHANGE_DenomPublicKey *denom1, - struct TALER_EXCHANGE_DenomPublicKey *denom2) +denoms_cmp (const struct TALER_EXCHANGE_DenomPublicKey *denom1, + const struct TALER_EXCHANGE_DenomPublicKey *denom2) { - struct GNUNET_CRYPTO_RsaPublicKey *tmp1; - struct GNUNET_CRYPTO_RsaPublicKey *tmp2; - int r1; - int r2; - int ret; - - /* First check if pub is the same. */ - if (0 != GNUNET_CRYPTO_rsa_public_key_cmp - (denom1->key.rsa_public_key, - denom2->key.rsa_public_key)) - return 1; + struct TALER_EXCHANGE_DenomPublicKey tmp1; + struct TALER_EXCHANGE_DenomPublicKey tmp2; - tmp1 = denom1->key.rsa_public_key; - tmp2 = denom2->key.rsa_public_key; - r1 = denom1->revoked; - r2 = denom2->revoked; - - denom1->key.rsa_public_key = NULL; - denom2->key.rsa_public_key = NULL; - /* Then proceed with the rest of the object. */ - ret = GNUNET_memcmp (denom1, - denom2); - denom1->revoked = r1; - denom2->revoked = r2; - denom1->key.rsa_public_key = tmp1; - denom2->key.rsa_public_key = tmp2; - return ret; + if (0 != + TALER_denom_pub_cmp (&denom1->key, + &denom2->key)) + return 1; + tmp1 = *denom1; + tmp2 = *denom2; + tmp1.revoked = false; + tmp2.revoked = false; + memset (&tmp1.key, + 0, + sizeof (tmp1.key)); + memset (&tmp2.key, + 0, + sizeof (tmp2.key)); + return GNUNET_memcmp (&tmp1, + &tmp2); } @@ -670,33 +767,27 @@ denoms_cmp (struct TALER_EXCHANGE_DenomPublicKey *denom1, * @return #GNUNET_OK on success, #GNUNET_SYSERR on error * (malformed JSON) */ -static int +static enum GNUNET_GenericReturnValue decode_keys_json (const json_t *resp_obj, bool check_sig, struct TALER_EXCHANGE_Keys *key_data, enum TALER_EXCHANGE_VersionCompatibility *vc) { - struct TALER_ExchangeSignatureP sig; - struct GNUNET_HashContext *hash_context; - struct TALER_ExchangePublicKeyP pub; - const char *currency; - struct GNUNET_JSON_Specification mspec[] = { - GNUNET_JSON_spec_fixed_auto ("eddsa_sig", - &sig), - GNUNET_JSON_spec_fixed_auto ("eddsa_pub", - &pub), - /* sig and pub must be first, as we skip those if - check_sig is false! */ - GNUNET_JSON_spec_fixed_auto ("master_public_key", - &key_data->master_pub), - TALER_JSON_spec_absolute_time ("list_issue_date", - &key_data->list_issue_date), - TALER_JSON_spec_relative_time ("reserve_closing_delay", - &key_data->reserve_closing_delay), - GNUNET_JSON_spec_string ("currency", - ¤cy), - GNUNET_JSON_spec_end () - }; + struct TALER_ExchangeSignatureP exchange_sig; + struct TALER_ExchangePublicKeyP exchange_pub; + const json_t *wblwk = NULL; + const json_t *global_fees; + const json_t *sign_keys_array; + const json_t *denominations_by_group; + const json_t *auditors_array; + const json_t *recoup_array = NULL; + const json_t *manifests = NULL; + bool no_extensions = false; + bool no_signature = false; + const json_t *accounts; + const json_t *fees; + const json_t *wads; + struct SignatureContext sig_ctx = { 0 }; if (JSON_OBJECT != json_typeof (resp_obj)) { @@ -708,16 +799,12 @@ decode_keys_json (const json_t *resp_obj, stderr, JSON_INDENT (2)); #endif - /* check the version */ + /* check the version first */ { - const char *ver; - unsigned int age; - unsigned int revision; - unsigned int current; - char dummy; + struct TALER_JSON_ProtocolVersion pv; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("version", - &ver), + TALER_JSON_spec_version ("version", + &pv), GNUNET_JSON_spec_end () }; @@ -729,140 +816,378 @@ decode_keys_json (const json_t *resp_obj, GNUNET_break_op (0); return GNUNET_SYSERR; } - if (3 != sscanf (ver, - "%u:%u:%u%c", - ¤t, - &revision, - &age, - &dummy)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } *vc = TALER_EXCHANGE_VC_MATCH; - if (EXCHANGE_PROTOCOL_CURRENT < current) + if (EXCHANGE_PROTOCOL_CURRENT < pv.current) { *vc |= TALER_EXCHANGE_VC_NEWER; - if (EXCHANGE_PROTOCOL_CURRENT < current - age) + if (EXCHANGE_PROTOCOL_CURRENT < pv.current - pv.age) *vc |= TALER_EXCHANGE_VC_INCOMPATIBLE; } - if (EXCHANGE_PROTOCOL_CURRENT > current) + if (EXCHANGE_PROTOCOL_CURRENT > pv.current) { *vc |= TALER_EXCHANGE_VC_OLDER; - if (EXCHANGE_PROTOCOL_CURRENT - EXCHANGE_PROTOCOL_AGE > current) + if (EXCHANGE_PROTOCOL_CURRENT - EXCHANGE_PROTOCOL_AGE > pv.current) *vc |= TALER_EXCHANGE_VC_INCOMPATIBLE; } - key_data->version = GNUNET_strdup (ver); } - hash_context = NULL; - EXITIF (GNUNET_OK != + { + const char *ver; + const char *currency; + const char *asset_type; + struct GNUNET_JSON_Specification mspec[] = { + GNUNET_JSON_spec_fixed_auto ( + "exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ( + "exchange_pub", + &exchange_pub), + GNUNET_JSON_spec_fixed_auto ( + "master_public_key", + &key_data->master_pub), + GNUNET_JSON_spec_array_const ("accounts", + &accounts), + GNUNET_JSON_spec_object_const ("wire_fees", + &fees), + GNUNET_JSON_spec_array_const ("wads", + &wads), + GNUNET_JSON_spec_timestamp ( + "list_issue_date", + &key_data->list_issue_date), + GNUNET_JSON_spec_relative_time ( + "reserve_closing_delay", + &key_data->reserve_closing_delay), + GNUNET_JSON_spec_string ( + "currency", + ¤cy), + GNUNET_JSON_spec_string ( + "asset_type", + &asset_type), + GNUNET_JSON_spec_array_const ( + "global_fees", + &global_fees), + GNUNET_JSON_spec_array_const ( + "signkeys", + &sign_keys_array), + GNUNET_JSON_spec_array_const ( + "denominations", + &denominations_by_group), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ( + "recoup", + &recoup_array), + NULL), + GNUNET_JSON_spec_array_const ( + "auditors", + &auditors_array), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ( + "rewards_allowed", + &key_data->rewards_allowed), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("extensions", + &manifests), + &no_extensions), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ( + "extensions_sig", + &key_data->extensions_sig), + &no_signature), + GNUNET_JSON_spec_string ("version", + &ver), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ( + "wallet_balance_limit_without_kyc", + &wblwk), + NULL), + GNUNET_JSON_spec_end () + }; + const char *emsg; + unsigned int eline; + + if (GNUNET_OK != + GNUNET_JSON_parse (resp_obj, + (check_sig) ? mspec : &mspec[2], + &emsg, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Parsing /keys failed for `%s' (%u)\n", + emsg, + eline); + EXITIF (1); + } + { + struct GNUNET_JSON_Specification sspec[] = { + TALER_JSON_spec_currency_specification ( + "currency_specification", + currency, + &key_data->cspec), + TALER_JSON_spec_amount ( + "stefan_abs", + currency, + &key_data->stefan_abs), + TALER_JSON_spec_amount ( + "stefan_log", + currency, + &key_data->stefan_log), + GNUNET_JSON_spec_double ( + "stefan_lin", + &key_data->stefan_lin), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != GNUNET_JSON_parse (resp_obj, - (check_sig) ? mspec : &mspec[2], - NULL, NULL)); - key_data->currency = GNUNET_strdup (currency); - /* parse the master public key and issue date of the response */ - if (check_sig) - hash_context = GNUNET_CRYPTO_hash_context_start (); + sspec, + &emsg, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Parsing /keys failed for `%s' (%u)\n", + emsg, + eline); + EXITIF (1); + } + } + + key_data->currency = GNUNET_strdup (currency); + key_data->version = GNUNET_strdup (ver); + key_data->asset_type = GNUNET_strdup (asset_type); + if (! no_extensions) + key_data->extensions = json_incref ((json_t *) manifests); + } + + /* parse the global fees */ + EXITIF (json_array_size (global_fees) > UINT_MAX); + key_data->num_global_fees + = (unsigned int) json_array_size (global_fees); + if (0 != key_data->num_global_fees) + { + json_t *global_fee; + size_t index; + + key_data->global_fees + = GNUNET_new_array (key_data->num_global_fees, + struct TALER_EXCHANGE_GlobalFee); + json_array_foreach (global_fees, index, global_fee) + { + EXITIF (GNUNET_SYSERR == + parse_global_fee (&key_data->global_fees[index], + check_sig, + global_fee, + key_data)); + } + } /* parse the signing keys */ + EXITIF (json_array_size (sign_keys_array) > UINT_MAX); + key_data->num_sign_keys + = (unsigned int) json_array_size (sign_keys_array); + if (0 != key_data->num_sign_keys) { - json_t *sign_keys_array; json_t *sign_key_obj; - unsigned int index; + size_t index; + + key_data->sign_keys + = GNUNET_new_array (key_data->num_sign_keys, + struct TALER_EXCHANGE_SigningPublicKey); + json_array_foreach (sign_keys_array, index, sign_key_obj) { + EXITIF (GNUNET_SYSERR == + parse_json_signkey (&key_data->sign_keys[index], + check_sig, + sign_key_obj, + &key_data->master_pub)); + } + } - EXITIF (NULL == (sign_keys_array = - json_object_get (resp_obj, - "signkeys"))); - EXITIF (JSON_ARRAY != json_typeof (sign_keys_array)); - if (0 != (key_data->num_sign_keys = - json_array_size (sign_keys_array))) + /* Parse balance limits */ + if (NULL != wblwk) + { + EXITIF (json_array_size (wblwk) > UINT_MAX); + key_data->wblwk_length + = (unsigned int) json_array_size (wblwk); + key_data->wallet_balance_limit_without_kyc + = GNUNET_new_array (key_data->wblwk_length, + struct TALER_Amount); + for (unsigned int i = 0; i<key_data->wblwk_length; i++) { - key_data->sign_keys - = GNUNET_new_array (key_data->num_sign_keys, - struct TALER_EXCHANGE_SigningPublicKey); - json_array_foreach (sign_keys_array, index, sign_key_obj) { - EXITIF (GNUNET_SYSERR == - parse_json_signkey (&key_data->sign_keys[index], - check_sig, - sign_key_obj, - &key_data->master_pub)); - } + struct TALER_Amount *a = &key_data->wallet_balance_limit_without_kyc[i]; + const json_t *aj = json_array_get (wblwk, + i); + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount (NULL, + key_data->currency, + a), + GNUNET_JSON_spec_end () + }; + + EXITIF (GNUNET_OK != + GNUNET_JSON_parse (aj, + spec, + NULL, NULL)); } } - /* parse the denomination keys, merging with the - possibly EXISTING array as required (/keys cherry picking) */ + /* Parse wire accounts */ + key_data->fees = parse_fees (&key_data->master_pub, + key_data->currency, + fees, + &key_data->fees_len); + EXITIF (NULL == key_data->fees); + /* parse accounts */ + EXITIF (json_array_size (accounts) > UINT_MAX); + GNUNET_array_grow (key_data->accounts, + key_data->accounts_len, + json_array_size (accounts)); + EXITIF (GNUNET_OK != + TALER_EXCHANGE_parse_accounts (&key_data->master_pub, + accounts, + key_data->accounts_len, + key_data->accounts)); + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Parsed %u wire accounts from JSON\n", + key_data->accounts_len); + + + /* Parse the supported extension(s): age-restriction. */ + /* TODO: maybe lift all this into a FP in TALER_Extension ? */ + if (! no_extensions) { - json_t *denom_keys_array; - json_t *denom_key_obj; - unsigned int index; + if (no_signature) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "found extensions without signature\n"); + } + else + { + /* We have an extensions object. Verify its signature. */ + EXITIF (GNUNET_OK != + TALER_extensions_verify_manifests_signature ( + manifests, + &key_data->extensions_sig, + &key_data->master_pub)); + + /* Parse and set the the configuration of the extensions accordingly */ + EXITIF (GNUNET_OK != + TALER_extensions_load_manifests (manifests)); + } - EXITIF (NULL == (denom_keys_array = - json_object_get (resp_obj, - "denoms"))); - EXITIF (JSON_ARRAY != json_typeof (denom_keys_array)); + /* Assuming we might have now a new value for age_mask, set it in key_data */ + key_data->age_mask = TALER_extensions_get_age_restriction_mask (); + } - json_array_foreach (denom_keys_array, index, denom_key_obj) { - struct TALER_EXCHANGE_DenomPublicKey dk; - bool found = false; + /* + * Parse the denomination keys, merging with the + * possibly EXISTING array as required (/keys cherry picking). + * + * The denominations are grouped by common values of + * {cipher, value, fee, age_mask}. + */ + { + json_t *group_obj; + unsigned int group_idx; - memset (&dk, - 0, - sizeof (dk)); - EXITIF (GNUNET_SYSERR == - parse_json_denomkey (&dk, - check_sig, - denom_key_obj, - &key_data->master_pub, - hash_context)); + json_array_foreach (denominations_by_group, + group_idx, + group_obj) + { + /* First, parse { cipher, fees, value, age_mask, hash } of the current + group. */ + struct TALER_DenominationGroup group = {0}; + const json_t *denom_keys_array; + struct GNUNET_JSON_Specification group_spec[] = { + TALER_JSON_spec_denomination_group (NULL, + key_data->currency, + &group), + GNUNET_JSON_spec_array_const ("denoms", + &denom_keys_array), + GNUNET_JSON_spec_end () + }; + json_t *denom_key_obj; + unsigned int index; - for (unsigned int j = 0; - j<key_data->num_denom_keys; - j++) + EXITIF (GNUNET_SYSERR == + GNUNET_JSON_parse (group_obj, + group_spec, + NULL, + NULL)); + + /* Now, parse the individual denominations */ + json_array_foreach (denom_keys_array, + index, + denom_key_obj) { - if (0 == denoms_cmp (&dk, - &key_data->denom_keys[j])) + /* Set the common fields from the group for this particular + denomination. Required to make the validity check inside + parse_json_denomkey_partially pass */ + struct TALER_EXCHANGE_DenomPublicKey dk = { + .value = group.value, + .fees = group.fees, + .key.age_mask = group.age_mask + }; + bool found = false; + + EXITIF (GNUNET_SYSERR == + parse_json_denomkey_partially (&dk, + group.cipher, + check_sig, + denom_key_obj, + &key_data->master_pub, + group_idx, + index, + check_sig + ? &sig_ctx + : NULL)); + for (unsigned int j = 0; + j<key_data->num_denom_keys; + j++) { - found = true; - break; + if (0 == denoms_cmp (&dk, + &key_data->denom_keys[j])) + { + found = true; + break; + } } - } - if (found) - { - /* 0:0:0 did not support /keys cherry picking */ - TALER_LOG_DEBUG ("Skipping denomination key: already know it\n"); - GNUNET_CRYPTO_rsa_public_key_free (dk.key.rsa_public_key); - continue; - } - if (key_data->denom_keys_size == key_data->num_denom_keys) - GNUNET_array_grow (key_data->denom_keys, - key_data->denom_keys_size, - key_data->denom_keys_size * 2 + 2); - key_data->denom_keys[key_data->num_denom_keys++] = dk; - - /* Update "last_denom_issue_date" */ - TALER_LOG_DEBUG ("Adding denomination key that is valid_from %s\n", - GNUNET_STRINGS_absolute_time_to_string (dk.valid_from)); - key_data->last_denom_issue_date - = GNUNET_TIME_absolute_max (key_data->last_denom_issue_date, - dk.valid_from); - }; - } + + if (found) + { + /* 0:0:0 did not support /keys cherry picking */ + TALER_LOG_DEBUG ("Skipping denomination key: already know it\n"); + TALER_denom_pub_free (&dk.key); + continue; + } + + if (key_data->denom_keys_size == key_data->num_denom_keys) + GNUNET_array_grow (key_data->denom_keys, + key_data->denom_keys_size, + key_data->denom_keys_size * 2 + 2); + GNUNET_assert (key_data->denom_keys_size > + key_data->num_denom_keys); + GNUNET_assert (key_data->num_denom_keys < UINT_MAX); + key_data->denom_keys[key_data->num_denom_keys++] = dk; + + /* Update "last_denom_issue_date" */ + TALER_LOG_DEBUG ("Adding denomination key that is valid_until %s\n", + GNUNET_TIME_timestamp2s (dk.valid_from)); + key_data->last_denom_issue_date + = GNUNET_TIME_timestamp_max (key_data->last_denom_issue_date, + dk.valid_from); + }; /* end of json_array_foreach over denominations */ + } /* end of json_array_foreach over groups of denominations */ + } /* end of scope for group_ojb/group_idx */ /* parse the auditor information */ { - json_t *auditors_array; json_t *auditor_info; unsigned int index; - EXITIF (NULL == (auditors_array = - json_object_get (resp_obj, - "auditors"))); - EXITIF (JSON_ARRAY != json_typeof (auditors_array)); - /* Merge with the existing auditor information we have (/keys cherry picking) */ - json_array_foreach (auditors_array, index, auditor_info) { + json_array_foreach (auditors_array, index, auditor_info) + { struct TALER_EXCHANGE_AuditorInformation ai; bool found = false; @@ -921,174 +1246,94 @@ decode_keys_json (const json_t *resp_obj, GNUNET_array_grow (key_data->auditors, key_data->auditors_size, key_data->auditors_size * 2 + 2); + GNUNET_assert (key_data->auditors_size > + key_data->num_auditors); GNUNET_assert (NULL != ai.auditor_url); + GNUNET_assert (key_data->num_auditors < UINT_MAX); key_data->auditors[key_data->num_auditors++] = ai; }; } /* parse the revocation/recoup information */ + if (NULL != recoup_array) { - json_t *recoup_array; json_t *recoup_info; unsigned int index; - if (NULL != (recoup_array = - json_object_get (resp_obj, - "recoup"))) + json_array_foreach (recoup_array, index, recoup_info) { - EXITIF (JSON_ARRAY != json_typeof (recoup_array)); - - json_array_foreach (recoup_array, index, recoup_info) { - struct GNUNET_HashCode h_denom_pub; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &h_denom_pub), - GNUNET_JSON_spec_end () - }; + struct TALER_DenominationHashP h_denom_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_denom_pub", + &h_denom_pub), + GNUNET_JSON_spec_end () + }; - EXITIF (GNUNET_OK != - GNUNET_JSON_parse (recoup_info, - spec, - NULL, NULL)); - for (unsigned int j = 0; - j<key_data->num_denom_keys; - j++) + EXITIF (GNUNET_OK != + GNUNET_JSON_parse (recoup_info, + spec, + NULL, NULL)); + for (unsigned int j = 0; + j<key_data->num_denom_keys; + j++) + { + if (0 == GNUNET_memcmp (&h_denom_pub, + &key_data->denom_keys[j].h_key)) { - if (0 == GNUNET_memcmp (&h_denom_pub, - &key_data->denom_keys[j].h_key)) - { - key_data->denom_keys[j].revoked = GNUNET_YES; - break; - } + key_data->denom_keys[j].revoked = true; + break; } - }; + } } } if (check_sig) { - struct TALER_ExchangeKeySetPS ks = { - .purpose.size = htonl (sizeof (ks)), - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_KEY_SET), - .list_issue_date = GNUNET_TIME_absolute_hton (key_data->list_issue_date) - }; + struct GNUNET_HashContext *hash_context; + struct GNUNET_HashCode hc; + hash_context = GNUNET_CRYPTO_hash_context_start (); + qsort (sig_ctx.elements, + sig_ctx.elements_pos, + sizeof (struct SignatureElement), + &signature_context_sort_cb); + for (unsigned int i = 0; i<sig_ctx.elements_pos; i++) + { + struct SignatureElement *element = &sig_ctx.elements[i]; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Adding %u,%u,%s\n", + element->group_offset, + element->offset, + TALER_B2S (&element->master_sig)); + GNUNET_CRYPTO_hash_context_read (hash_context, + &element->master_sig, + sizeof (element->master_sig)); + } + GNUNET_array_grow (sig_ctx.elements, + sig_ctx.elements_size, + 0); GNUNET_CRYPTO_hash_context_finish (hash_context, - &ks.hc); - hash_context = NULL; + &hc); EXITIF (GNUNET_OK != TALER_EXCHANGE_test_signing_key (key_data, - &pub)); + &exchange_pub)); EXITIF (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_EXCHANGE_KEY_SET, - &ks, - &sig.eddsa_signature, - &pub.eddsa_pub)); + TALER_exchange_online_key_set_verify ( + key_data->list_issue_date, + &hc, + &exchange_pub, + &exchange_sig)); } return GNUNET_OK; -EXITIF_exit: +EXITIF_exit: *vc = TALER_EXCHANGE_VC_PROTOCOL_ERROR; - if (NULL != hash_context) - GNUNET_CRYPTO_hash_context_abort (hash_context); return GNUNET_SYSERR; } /** - * Free key data object. - * - * @param key_data data to free (pointer itself excluded) - */ -static void -free_key_data (struct TALER_EXCHANGE_Keys *key_data) -{ - GNUNET_array_grow (key_data->sign_keys, - key_data->num_sign_keys, - 0); - for (unsigned int i = 0; i<key_data->num_denom_keys; i++) - GNUNET_CRYPTO_rsa_public_key_free ( - key_data->denom_keys[i].key.rsa_public_key); - - GNUNET_array_grow (key_data->denom_keys, - key_data->denom_keys_size, - 0); - for (unsigned int i = 0; i<key_data->num_auditors; i++) - { - GNUNET_array_grow (key_data->auditors[i].denom_keys, - key_data->auditors[i].num_denom_keys, - 0); - GNUNET_free (key_data->auditors[i].auditor_url); - } - GNUNET_array_grow (key_data->auditors, - key_data->auditors_size, - 0); - GNUNET_free (key_data->version); - key_data->version = NULL; - GNUNET_free (key_data->currency); - key_data->currency = NULL; -} - - -/** - * Initiate download of /keys from the exchange. - * - * @param cls exchange where to download /keys from - */ -static void -request_keys (void *cls); - - -/** - * Let the user set the last valid denomination time manually. - * - * @param exchange the exchange handle. - * @param last_denom_new new last denomination time. - */ -void -TALER_EXCHANGE_set_last_denom (struct TALER_EXCHANGE_Handle *exchange, - struct GNUNET_TIME_Absolute last_denom_new) -{ - exchange->key_data.last_denom_issue_date = last_denom_new; -} - - -/** - * Check if our current response for /keys is valid, and if - * not trigger download. - * - * @param exchange exchange to check keys for - * @param flags options controlling when to download what - * @return until when the response is current, 0 if we are re-downloading - */ -struct GNUNET_TIME_Absolute -TALER_EXCHANGE_check_keys_current (struct TALER_EXCHANGE_Handle *exchange, - enum TALER_EXCHANGE_CheckKeysFlags flags) -{ - bool force_download = 0 != (flags & TALER_EXCHANGE_CKF_FORCE_DOWNLOAD); - bool pull_all_keys = 0 != (flags & TALER_EXCHANGE_CKF_PULL_ALL_KEYS); - - if (NULL != exchange->kr) - return GNUNET_TIME_UNIT_ZERO_ABS; - - if (pull_all_keys) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Forcing re-download of all exchange keys\n"); - GNUNET_break (GNUNET_YES == force_download); - exchange->state = MHS_INIT; - } - if ( (! force_download) && - (GNUNET_TIME_absolute_is_future (exchange->key_data_expiration)) ) - return exchange->key_data_expiration; - if (NULL == exchange->retry_task) - exchange->retry_task = GNUNET_SCHEDULER_add_now (&request_keys, - exchange); - return GNUNET_TIME_UNIT_ZERO_ABS; -} - - -/** * Callback used when downloading the reply to a /keys request * is complete. * @@ -1101,119 +1346,122 @@ keys_completed_cb (void *cls, long response_code, const void *resp_obj) { - struct KeysRequest *kr = cls; - struct TALER_EXCHANGE_Handle *exchange = kr->exchange; - struct TALER_EXCHANGE_Keys kd; - struct TALER_EXCHANGE_Keys kd_old; - enum TALER_EXCHANGE_VersionCompatibility vc; + struct TALER_EXCHANGE_GetKeysHandle *gkh = cls; const json_t *j = resp_obj; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + 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.\n", - kr->url, - response_code); - kd_old = exchange->key_data; - memset (&kd, - 0, - sizeof (struct TALER_EXCHANGE_Keys)); - vc = TALER_EXCHANGE_VC_PROTOCOL_ERROR; + "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: - free_keys_request (kr); - exchange->keys_error_count++; - exchange->kr = NULL; - GNUNET_assert (NULL == exchange->retry_task); - exchange->retry_delay = EXCHANGE_LIB_BACKOFF (exchange->retry_delay); - exchange->retry_task = GNUNET_SCHEDULER_add_delayed (exchange->retry_delay, - &request_keys, - exchange); - return; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to receive /keys response from exchange %s\n", + gkh->exchange_url); + break; case MHD_HTTP_OK: - exchange->keys_error_count = 0; if (NULL == j) { + GNUNET_break (0); response_code = 0; break; } - /* 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... */ - 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++) - kd.denom_keys[i].key.rsa_public_key - = GNUNET_CRYPTO_rsa_public_key_dup ( - kd_old.denom_keys[i].key.rsa_public_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++) + kd = GNUNET_new (struct TALER_EXCHANGE_Keys); + kd->exchange_url = GNUNET_strdup (gkh->exchange_url); + if (NULL != gkh->prev_keys) { - const struct TALER_EXCHANGE_AuditorInformation *aold = - &kd_old.auditors[i]; - struct TALER_EXCHANGE_AuditorInformation *anew = &kd.auditors[i]; - - anew->auditor_pub = aold->auditor_pub; - GNUNET_assert (NULL != aold->auditor_url); - anew->auditor_url = GNUNET_strdup (aold->auditor_url); - GNUNET_array_grow (anew->denom_keys, - anew->num_denom_keys, - aold->num_denom_keys); - memcpy (anew->denom_keys, - aold->denom_keys, - aold->num_denom_keys - * sizeof (struct TALER_EXCHANGE_AuditorDenominationInfo)); - } + 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]; - /* Old auditors got just copied into new ones. */ + anew->auditor_pub = aold->auditor_pub; + anew->auditor_url = GNUNET_strdup (aold->auditor_url); + 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 != decode_keys_json (j, true, - &kd, - &vc)) + kd, + &kresp.details.ok.compat)) { TALER_LOG_ERROR ("Could not decode /keys response\n"); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - for (unsigned int i = 0; i<kd.num_auditors; i++) - { - struct TALER_EXCHANGE_AuditorInformation *anew = &kd.auditors[i]; - - GNUNET_array_grow (anew->denom_keys, - anew->num_denom_keys, - 0); - GNUNET_free (anew->auditor_url); - } - GNUNET_free (kd.auditors); - kd.auditors = NULL; - kd.num_auditors = 0; - for (unsigned int i = 0; i<kd_old.num_denom_keys; i++) - GNUNET_CRYPTO_rsa_public_key_free (kd.denom_keys[i].key.rsa_public_key); - GNUNET_array_grow (kd.denom_keys, - kd.denom_keys_size, - 0); - kd.num_denom_keys = 0; + kd->rc = 1; + TALER_EXCHANGE_keys_decref (kd); + kd = NULL; + kresp.hr.http_status = 0; + kresp.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } - json_decref (exchange->key_data_raw); - exchange->key_data_raw = json_deep_copy (j); - exchange->retry_delay = GNUNET_TIME_UNIT_ZERO; + 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: @@ -1221,121 +1469,36 @@ keys_completed_cb (void *cls, case MHD_HTTP_NOT_FOUND: if (NULL == j) { - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = TALER_ErrorCode_get_hint (hr.ec); + kresp.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + kresp.hr.hint = TALER_ErrorCode_get_hint (kresp.hr.ec); } else { - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + kresp.hr.ec = TALER_JSON_get_error_code (j); + kresp.hr.hint = TALER_JSON_get_error_hint (j); } break; default: - if (MHD_HTTP_GATEWAY_TIMEOUT == response_code) - exchange->keys_error_count++; if (NULL == j) { - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = TALER_ErrorCode_get_hint (hr.ec); + kresp.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + kresp.hr.hint = TALER_ErrorCode_get_hint (kresp.hr.ec); } else { - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + 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) hr.ec); + (int) kresp.hr.ec); break; } - exchange->key_data = kd; - TALER_LOG_DEBUG ("Last DK issue date update to: %s\n", - GNUNET_STRINGS_absolute_time_to_string - (exchange->key_data.last_denom_issue_date)); - - - if (MHD_HTTP_OK != response_code) - { - exchange->kr = NULL; - free_keys_request (kr); - exchange->state = MHS_FAILED; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange keys download failed\n"); - if (NULL != exchange->key_data_raw) - { - json_decref (exchange->key_data_raw); - exchange->key_data_raw = NULL; - } - free_key_data (&kd_old); - /* notify application that we failed */ - exchange->cert_cb (exchange->cert_cb_cls, - &hr, - NULL, - vc); - return; - } - - exchange->kr = NULL; - exchange->key_data_expiration = kr->expire; - free_keys_request (kr); - exchange->state = MHS_CERT; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Successfully downloaded exchange's keys\n"); - update_auditors (exchange); - /* notify application about the key information */ - exchange->cert_cb (exchange->cert_cb_cls, - &hr, - &exchange->key_data, - vc); - free_key_data (&kd_old); -} - - -/* ********************* library internal API ********* */ - - -/** - * Get the context of a exchange. - * - * @param h the exchange handle to query - * @return ctx context to execute jobs in - */ -struct GNUNET_CURL_Context * -TEAH_handle_to_context (struct TALER_EXCHANGE_Handle *h) -{ - return h->ctx; -} - - -/** - * Check if the handle is ready to process requests. - * - * @param h the exchange handle to query - * @return #GNUNET_YES if we are ready, #GNUNET_NO if not - */ -int -TEAH_handle_is_ready (struct TALER_EXCHANGE_Handle *h) -{ - return (MHS_CERT == h->state) ? GNUNET_YES : GNUNET_NO; -} - - -/** - * Obtain the URL to use for an API request. - * - * @param h handle for the exchange - * @param path Taler API path (i.e. "/reserve/withdraw") - * @return the full URL to use with cURL - */ -char * -TEAH_path_to_url (struct TALER_EXCHANGE_Handle *h, - const char *path) -{ - GNUNET_assert ('/' == path[0]); - return TALER_url_join (h->url, - path + 1, - NULL); + gkh->cert_cb (gkh->cert_cb_cls, + &kresp, + kd); + TALER_EXCHANGE_get_keys_cancel (gkh); } @@ -1349,12 +1512,12 @@ TEAH_path_to_url (struct TALER_EXCHANGE_Handle *h, * Parse HTTP timestamp. * * @param dateline header to parse header - * @param at where to write the result + * @param[out] at where to write the result * @return #GNUNET_OK on success */ -static int +static enum GNUNET_GenericReturnValue parse_date_string (const char *dateline, - struct GNUNET_TIME_Absolute *at) + struct GNUNET_TIME_Timestamp *at) { static const char *MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", @@ -1436,7 +1599,7 @@ parse_date_string (const char *dateline, } if (t < 0) t = 0; /* can happen due to timezone issues if date was 1.1.1970 */ - at->abs_value_us = 1000LL * 1000LL * t; + *at = GNUNET_TIME_timestamp_from_s (t); return GNUNET_OK; } @@ -1449,7 +1612,7 @@ parse_date_string (const char *dateline, * @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 KeysRequest` + * @param userdata the `struct TALER_EXCHANGE_GetKeysHandle` * @return `size * nitems` on success (everything else aborts) */ static size_t @@ -1458,7 +1621,7 @@ header_cb (char *buffer, size_t nitems, void *userdata) { - struct KeysRequest *kr = userdata; + struct TALER_EXCHANGE_GetKeysHandle *kr = userdata; size_t total = size * nitems; char *val; @@ -1470,6 +1633,10 @@ header_cb (char *buffer, 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)) @@ -1478,205 +1645,579 @@ header_cb (char *buffer, "Failed to parse %s-header `%s'\n", MHD_HTTP_HEADER_EXPIRES, val); - kr->expire = GNUNET_TIME_UNIT_ZERO_ABS; + kr->expire = GNUNET_TIME_UNIT_ZERO_TS; } GNUNET_free (val); return total; } -/* ********************* public API ******************* */ +struct TALER_EXCHANGE_GetKeysHandle * +TALER_EXCHANGE_get_keys ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *last_keys, + TALER_EXCHANGE_GetKeysCallback cert_cb, + void *cert_cb_cls) +{ + struct TALER_EXCHANGE_GetKeysHandle *gkh; + CURL *eh; + char last_date[80] = { 0 }; + TALER_LOG_DEBUG ("Connecting to the exchange (%s)\n", + url); + gkh = GNUNET_new (struct TALER_EXCHANGE_GetKeysHandle); + gkh->exchange_url = GNUNET_strdup (url); + gkh->cert_cb = cert_cb; + gkh->cert_cb_cls = cert_cb_cls; + if (NULL != last_keys) + { + gkh->prev_keys = TALER_EXCHANGE_keys_incref (last_keys); + TALER_LOG_DEBUG ("Last DK issue date (before GETting /keys): %s\n", + GNUNET_TIME_timestamp2s ( + last_keys->last_denom_issue_date)); + GNUNET_snprintf (last_date, + sizeof (last_date), + "%llu", + (unsigned long long) + last_keys->last_denom_issue_date.abs_time.abs_value_us + / 1000000LLU); + } + gkh->url = TALER_url_join (url, + "keys", + (NULL != last_keys) + ? "last_issue_date" + : NULL, + (NULL != last_keys) + ? last_date + : NULL, + NULL); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "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->exchange_url); + GNUNET_free (gkh->url); + GNUNET_free (gkh); + return NULL; + } + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_VERBOSE, + 1)); + 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 (ctx, + eh, + &keys_completed_cb, + gkh); + return gkh; +} -/** - * Deserialize the key data and use it to bootstrap @a exchange to - * more efficiently recover the state. Errors in @a data must be - * tolerated (i.e. by re-downloading instead). - * - * @param exchange which exchange's key and wire data should be deserialized - * @param data the data to deserialize - */ -static void -deserialize_data (struct TALER_EXCHANGE_Handle *exchange, - const json_t *data) + +void +TALER_EXCHANGE_get_keys_cancel ( + struct TALER_EXCHANGE_GetKeysHandle *gkh) { - enum TALER_EXCHANGE_VersionCompatibility vc; - json_t *keys; + 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); +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_test_signing_key ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ExchangePublicKeyP *pub) +{ + struct GNUNET_TIME_Absolute now; + + /* we will check using a tolerance of 1h for the time */ + now = GNUNET_TIME_absolute_get (); + for (unsigned int i = 0; i<keys->num_sign_keys; i++) + if ( (GNUNET_TIME_absolute_cmp ( + keys->sign_keys[i].valid_from.abs_time, + <=, + GNUNET_TIME_absolute_add (now, + LIFETIME_TOLERANCE))) && + (GNUNET_TIME_absolute_cmp ( + keys->sign_keys[i].valid_until.abs_time, + >, + GNUNET_TIME_absolute_subtract (now, + LIFETIME_TOLERANCE))) && + (0 == GNUNET_memcmp (pub, + &keys->sign_keys[i].key)) ) + return GNUNET_OK; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Signing key not valid at time %s\n", + GNUNET_TIME_absolute2s (now)); + return GNUNET_SYSERR; +} + + +const struct TALER_EXCHANGE_DenomPublicKey * +TALER_EXCHANGE_get_denomination_key ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_DenominationPublicKey *pk) +{ + for (unsigned int i = 0; i<keys->num_denom_keys; i++) + if (0 == + TALER_denom_pub_cmp (pk, + &keys->denom_keys[i].key)) + return &keys->denom_keys[i]; + return NULL; +} + + +const struct TALER_EXCHANGE_GlobalFee * +TALER_EXCHANGE_get_global_fee ( + const struct TALER_EXCHANGE_Keys *keys, + struct GNUNET_TIME_Timestamp ts) +{ + for (unsigned int i = 0; i<keys->num_global_fees; i++) + { + const struct TALER_EXCHANGE_GlobalFee *gf = &keys->global_fees[i]; + + if (GNUNET_TIME_timestamp_cmp (ts, + >=, + gf->start_date) && + GNUNET_TIME_timestamp_cmp (ts, + <, + gf->end_date)) + return gf; + } + return NULL; +} + + +struct TALER_EXCHANGE_DenomPublicKey * +TALER_EXCHANGE_copy_denomination_key ( + const struct TALER_EXCHANGE_DenomPublicKey *key) +{ + struct TALER_EXCHANGE_DenomPublicKey *copy; + + copy = GNUNET_new (struct TALER_EXCHANGE_DenomPublicKey); + *copy = *key; + TALER_denom_pub_copy (©->key, + &key->key); + return copy; +} + + +void +TALER_EXCHANGE_destroy_denomination_key ( + struct TALER_EXCHANGE_DenomPublicKey *key) +{ + TALER_denom_pub_free (&key->key); + GNUNET_free (key); +} + + +const struct TALER_EXCHANGE_DenomPublicKey * +TALER_EXCHANGE_get_denomination_key_by_hash ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_DenominationHashP *hc) +{ + for (unsigned int i = 0; i<keys->num_denom_keys; i++) + if (0 == GNUNET_memcmp (hc, + &keys->denom_keys[i].h_key)) + return &keys->denom_keys[i]; + return NULL; +} + + +struct TALER_EXCHANGE_Keys * +TALER_EXCHANGE_keys_incref (struct TALER_EXCHANGE_Keys *keys) +{ + GNUNET_assert (keys->rc < UINT_MAX); + keys->rc++; + return keys; +} + + +void +TALER_EXCHANGE_keys_decref (struct TALER_EXCHANGE_Keys *keys) +{ + if (NULL == keys) + return; + GNUNET_assert (0 < keys->rc); + keys->rc--; + if (0 != keys->rc) + return; + GNUNET_array_grow (keys->sign_keys, + keys->num_sign_keys, + 0); + for (unsigned int i = 0; i<keys->num_denom_keys; i++) + TALER_denom_pub_free (&keys->denom_keys[i].key); + + GNUNET_array_grow (keys->denom_keys, + keys->denom_keys_size, + 0); + for (unsigned int i = 0; i<keys->num_auditors; i++) + { + GNUNET_array_grow (keys->auditors[i].denom_keys, + keys->auditors[i].num_denom_keys, + 0); + GNUNET_free (keys->auditors[i].auditor_url); + } + GNUNET_array_grow (keys->auditors, + keys->auditors_size, + 0); + TALER_EXCHANGE_free_accounts (keys->accounts_len, + keys->accounts); + GNUNET_array_grow (keys->accounts, + keys->accounts_len, + 0); + free_fees (keys->fees, + keys->fees_len); + json_decref (keys->extensions); + GNUNET_free (keys->cspec.name); + json_decref (keys->cspec.map_alt_unit_names); + GNUNET_free (keys->wallet_balance_limit_without_kyc); + GNUNET_free (keys->version); + GNUNET_free (keys->currency); + GNUNET_free (keys->asset_type); + GNUNET_free (keys->global_fees); + GNUNET_free (keys->exchange_url); + GNUNET_free (keys); +} + + +struct TALER_EXCHANGE_Keys * +TALER_EXCHANGE_keys_from_json (const json_t *j) +{ + const json_t *jkeys; const char *url; uint32_t version; - struct GNUNET_TIME_Absolute expire; + struct GNUNET_TIME_Timestamp expire + = GNUNET_TIME_UNIT_ZERO_TS; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_uint32 ("version", &version), - GNUNET_JSON_spec_json ("keys", - &keys), - GNUNET_JSON_spec_string ("exchange_url", + GNUNET_JSON_spec_object_const ("keys", + &jkeys), + TALER_JSON_spec_web_url ("exchange_url", &url), - TALER_JSON_spec_absolute_time ("expire", - &expire), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("expire", + &expire), + NULL), GNUNET_JSON_spec_end () }; - struct TALER_EXCHANGE_Keys key_data; - struct TALER_EXCHANGE_HttpResponse hr = { - .ec = TALER_EC_NONE, - .http_status = MHD_HTTP_OK, - .reply = data - }; + struct TALER_EXCHANGE_Keys *keys; + enum TALER_EXCHANGE_VersionCompatibility compat; - if (NULL == data) - return; + if (NULL == j) + return NULL; if (GNUNET_OK != - GNUNET_JSON_parse (data, + GNUNET_JSON_parse (j, spec, NULL, NULL)) { GNUNET_break_op (0); - return; + return NULL; } if (0 != version) { - GNUNET_JSON_parse_free (spec); - return; /* unsupported version */ + return NULL; /* unsupported version */ } - if (0 != strcmp (url, - exchange->url)) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return; - } - memset (&key_data, - 0, - sizeof (struct TALER_EXCHANGE_Keys)); + keys = GNUNET_new (struct TALER_EXCHANGE_Keys); if (GNUNET_OK != - decode_keys_json (keys, + decode_keys_json (jkeys, false, - &key_data, - &vc)) + keys, + &compat)) { GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return; + return NULL; } - /* decode successful, initialize with the result */ - GNUNET_assert (NULL == exchange->key_data_raw); - exchange->key_data_raw = json_deep_copy (keys); - exchange->key_data = key_data; - exchange->key_data_expiration = expire; - exchange->state = MHS_CERT; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Successfully loaded exchange's keys via deserialization\n"); - update_auditors (exchange); - /* notify application about the key information */ - exchange->cert_cb (exchange->cert_cb_cls, - &hr, - &exchange->key_data, - vc); - GNUNET_JSON_parse_free (spec); + keys->rc = 1; + keys->key_data_expiration = expire; + keys->exchange_url = GNUNET_strdup (url); + return keys; } /** - * Serialize the latest key data from @a - * exchange to be persisted on disk (to be used with - * #TALER_EXCHANGE_OPTION_DATA to more efficiently recover - * the state). + * Data we track per denomination group. + */ +struct GroupData +{ + /** + * The json blob with the group meta-data and list of denominations + */ + json_t *json; + + /** + * Meta data for this group. + */ + struct TALER_DenominationGroup meta; +}; + + +/** + * Add denomination group represented by @a value + * to list of denominations in @a cls. Also frees + * the @a value. * - * @param exchange which exchange's key and wire data should be - * serialized - * @return NULL on error (i.e. no current data available); - * otherwise JSON object owned by the caller + * @param[in,out] cls a `json_t *` with an array to build + * @param key unused + * @param value a `struct GroupData *` + * @return #GNUNET_OK (continue to iterate) */ +static enum GNUNET_GenericReturnValue +add_grp (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + json_t *denominations_by_group = cls; + struct GroupData *gd = value; + const char *cipher; + json_t *ge; + bool age_restricted = gd->meta.age_mask.bits != 0; + + (void) key; + switch (gd->meta.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + cipher = age_restricted ? "RSA+age_restricted" : "RSA"; + break; + case GNUNET_CRYPTO_BSA_CS: + cipher = age_restricted ? "CS+age_restricted" : "CS"; + break; + default: + GNUNET_assert (false); + } + + ge = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + cipher), + GNUNET_JSON_pack_array_steal ("denoms", + gd->json), + TALER_JSON_PACK_DENOM_FEES ("fee", + &gd->meta.fees), + GNUNET_JSON_pack_allow_null ( + age_restricted + ? GNUNET_JSON_pack_uint64 ("age_mask", + gd->meta.age_mask.bits) + : GNUNET_JSON_pack_string ("dummy", + NULL)), + TALER_JSON_pack_amount ("value", + &gd->meta.value)); + GNUNET_assert (0 == + json_array_append_new (denominations_by_group, + ge)); + GNUNET_free (gd); + return GNUNET_OK; +} + + +/** + * Convert array of account restrictions @a ars to JSON. + * + * @param ar_len length of @a ars + * @param ars account restrictions to convert + * @return JSON representation + */ +static json_t * +ar_to_json (unsigned int ar_len, + const struct TALER_EXCHANGE_AccountRestriction ars[static ar_len]) +{ + json_t *rval; + + rval = json_array (); + GNUNET_assert (NULL != rval); + for (unsigned int i = 0; i<ar_len; i++) + { + const struct TALER_EXCHANGE_AccountRestriction *ar = &ars[i]; + + switch (ar->type) + { + case TALER_EXCHANGE_AR_INVALID: + GNUNET_break (0); + json_decref (rval); + return NULL; + case TALER_EXCHANGE_AR_DENY: + GNUNET_assert ( + 0 == + json_array_append_new ( + rval, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "deny")))); + break; + case TALER_EXCHANGE_AR_REGEX: + GNUNET_assert ( + 0 == + json_array_append_new ( + rval, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ( + "type", + "regex"), + GNUNET_JSON_pack_string ( + "payto_regex", + ar->details.regex.posix_egrep), + GNUNET_JSON_pack_string ( + "human_hint", + ar->details.regex.human_hint), + GNUNET_JSON_pack_object_incref ( + "human_hint_i18n", + (json_t *) ar->details.regex.human_hint_i18n) + ))); + break; + } + } + return rval; +} + + json_t * -TALER_EXCHANGE_serialize_data (struct TALER_EXCHANGE_Handle *exchange) +TALER_EXCHANGE_keys_to_json (const struct TALER_EXCHANGE_Keys *kd) { - const struct TALER_EXCHANGE_Keys *kd = &exchange->key_data; - struct GNUNET_TIME_Absolute now; + struct GNUNET_TIME_Timestamp now; json_t *keys; json_t *signkeys; - json_t *denoms; + json_t *denominations_by_group; json_t *auditors; + json_t *recoup; + json_t *wire_fees; + json_t *accounts; + json_t *global_fees; + json_t *wblwk = NULL; - now = GNUNET_TIME_absolute_get (); + now = GNUNET_TIME_timestamp_get (); signkeys = json_array (); - if (NULL == signkeys) - { - GNUNET_break (0); - return NULL; - } + GNUNET_assert (NULL != signkeys); for (unsigned int i = 0; i<kd->num_sign_keys; i++) { const struct TALER_EXCHANGE_SigningPublicKey *sk = &kd->sign_keys[i]; json_t *signkey; - if (now.abs_value_us > sk->valid_until.abs_value_us) + if (GNUNET_TIME_timestamp_cmp (now, + >, + sk->valid_until)) continue; /* skip keys that have expired */ signkey = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("key", &sk->key), GNUNET_JSON_pack_data_auto ("master_sig", &sk->master_sig), - GNUNET_JSON_pack_time_abs ("stamp_start", - sk->valid_from), - GNUNET_JSON_pack_time_abs ("stamp_expire", - sk->valid_until), - GNUNET_JSON_pack_time_abs ("stamp_end", - sk->valid_legal)); - if (NULL == signkey) - { - GNUNET_break (0); - continue; - } - if (0 != json_array_append_new (signkeys, - signkey)) - { - GNUNET_break (0); - json_decref (signkey); - json_decref (signkeys); - return NULL; - } - } - denoms = json_array (); - if (NULL == denoms) - { - GNUNET_break (0); - json_decref (signkeys); - return NULL; + GNUNET_JSON_pack_timestamp ("stamp_start", + sk->valid_from), + GNUNET_JSON_pack_timestamp ("stamp_expire", + sk->valid_until), + GNUNET_JSON_pack_timestamp ("stamp_end", + sk->valid_legal)); + GNUNET_assert (NULL != signkey); + GNUNET_assert (0 == + json_array_append_new (signkeys, + signkey)); } - for (unsigned int i = 0; i<kd->num_denom_keys; i++) + + denominations_by_group = json_array (); + GNUNET_assert (NULL != denominations_by_group); { - const struct TALER_EXCHANGE_DenomPublicKey *dk = &kd->denom_keys[i]; - json_t *denom; + struct GNUNET_CONTAINER_MultiHashMap *dbg; - if (now.abs_value_us > dk->expire_deposit.abs_value_us) - continue; /* skip keys that have expired */ - denom = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_time_abs ("stamp_expire_deposit", - dk->expire_deposit), - GNUNET_JSON_pack_time_abs ("stamp_expire_withdraw", - dk->withdraw_valid_until), - GNUNET_JSON_pack_time_abs ("stamp_start", - dk->valid_from), - GNUNET_JSON_pack_time_abs ("stamp_expire_legal", - dk->expire_legal), - TALER_JSON_pack_amount ("value", - &dk->value), - TALER_JSON_pack_amount ("fee_withdraw", - &dk->fee_withdraw), - TALER_JSON_pack_amount ("fee_deposit", - &dk->fee_deposit), - TALER_JSON_pack_amount ("fee_refresh", - &dk->fee_refresh), - TALER_JSON_pack_amount ("fee_refund", - &dk->fee_refund), - GNUNET_JSON_pack_data_auto ("master_sig", - &dk->master_sig), - TALER_JSON_pack_denomination_public_key ("denom_pub", - &dk->key)); - GNUNET_assert (0 == - json_array_append_new (denoms, - denom)); + dbg = GNUNET_CONTAINER_multihashmap_create (128, + false); + for (unsigned int i = 0; i<kd->num_denom_keys; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dk = &kd->denom_keys[i]; + struct TALER_DenominationGroup meta = { + .cipher = dk->key.bsign_pub_key->cipher, + .value = dk->value, + .fees = dk->fees, + .age_mask = dk->key.age_mask + }; + struct GNUNET_HashCode key; + struct GroupData *gd; + json_t *denom; + struct GNUNET_JSON_PackSpec key_spec; + + if (GNUNET_TIME_timestamp_cmp (now, + >, + dk->expire_deposit)) + continue; /* skip keys that have expired */ + TALER_denomination_group_get_key (&meta, + &key); + gd = GNUNET_CONTAINER_multihashmap_get (dbg, + &key); + if (NULL == gd) + { + gd = GNUNET_new (struct GroupData); + gd->meta = meta; + gd->json = json_array (); + GNUNET_assert (NULL != gd->json); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (dbg, + &key, + gd, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + + } + switch (meta.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + key_spec = + GNUNET_JSON_pack_rsa_public_key ( + "rsa_pub", + dk->key.bsign_pub_key->details.rsa_public_key); + break; + case GNUNET_CRYPTO_BSA_CS: + key_spec = + GNUNET_JSON_pack_data_varsize ( + "cs_pub", + &dk->key.bsign_pub_key->details.cs_public_key, + sizeof (dk->key.bsign_pub_key->details.cs_public_key)); + break; + default: + GNUNET_assert (false); + } + denom = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", + dk->expire_deposit), + GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", + dk->withdraw_valid_until), + GNUNET_JSON_pack_timestamp ("stamp_start", + dk->valid_from), + GNUNET_JSON_pack_timestamp ("stamp_expire_legal", + dk->expire_legal), + GNUNET_JSON_pack_data_auto ("master_sig", + &dk->master_sig), + key_spec + ); + GNUNET_assert (0 == + json_array_append_new (gd->json, + denom)); + } + GNUNET_CONTAINER_multihashmap_iterate (dbg, + &add_grp, + denominations_by_group); + GNUNET_CONTAINER_multihashmap_destroy (dbg); } + auditors = json_array (); GNUNET_assert (NULL != auditors); for (unsigned int i = 0; i<kd->num_auditors; i++) @@ -1686,14 +2227,7 @@ TALER_EXCHANGE_serialize_data (struct TALER_EXCHANGE_Handle *exchange) json_t *adenoms; adenoms = json_array (); - if (NULL == adenoms) - { - GNUNET_break (0); - json_decref (denoms); - json_decref (signkeys); - json_decref (auditors); - return NULL; - } + GNUNET_assert (NULL != adenoms); for (unsigned int j = 0; j<ai->num_denom_keys; j++) { const struct TALER_EXCHANGE_AuditorDenominationInfo *adi = @@ -1702,7 +2236,10 @@ TALER_EXCHANGE_serialize_data (struct TALER_EXCHANGE_Handle *exchange) &kd->denom_keys[adi->denom_key_offset]; json_t *k; - if (now.abs_value_us > dk->expire_deposit.abs_value_us) + GNUNET_assert (adi->denom_key_offset < kd->num_denom_keys); + if (GNUNET_TIME_timestamp_cmp (now, + >, + dk->expire_deposit)) continue; /* skip auditor signatures for denomination keys that have expired */ GNUNET_assert (adi->denom_key_offset < kd->num_denom_keys); k = GNUNET_JSON_PACK ( @@ -1726,420 +2263,209 @@ TALER_EXCHANGE_serialize_data (struct TALER_EXCHANGE_Handle *exchange) json_array_append_new (auditors, a)); } - keys = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("version", - kd->version), - GNUNET_JSON_pack_string ("currency", - kd->currency), - GNUNET_JSON_pack_data_auto ("master_public_key", - &kd->master_pub), - GNUNET_JSON_pack_time_rel ("reserve_closing_delay", - kd->reserve_closing_delay), - GNUNET_JSON_pack_time_abs ("list_issue_date", - kd->list_issue_date), - GNUNET_JSON_pack_array_steal ("signkeys", - signkeys), - GNUNET_JSON_pack_array_steal ("denoms", - denoms), - GNUNET_JSON_pack_array_steal ("auditors", - auditors)); - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("version", - EXCHANGE_SERIALIZATION_FORMAT_VERSION), - GNUNET_JSON_pack_time_abs ("expire", - exchange->key_data_expiration), - GNUNET_JSON_pack_string ("exchange_url", - exchange->url), - GNUNET_JSON_pack_object_steal ("keys", - keys)); -} - - -/** - * Initialise a connection to the exchange. Will connect to the - * exchange and obtain information about the exchange's master - * public key and the exchange's auditor. - * The respective information will be passed to the @a cert_cb - * once available, and all future interactions with the exchange - * will be checked to be signed (where appropriate) by the - * respective master key. - * - * @param ctx the context - * @param url HTTP base URL for the exchange - * @param cert_cb function to call with the exchange's - * certification information - * @param cert_cb_cls closure for @a cert_cb - * @param ... list of additional arguments, - * terminated by #TALER_EXCHANGE_OPTION_END. - * @return the exchange handle; NULL upon error - */ -struct TALER_EXCHANGE_Handle * -TALER_EXCHANGE_connect ( - struct GNUNET_CURL_Context *ctx, - const char *url, - TALER_EXCHANGE_CertificationCallback cert_cb, - void *cert_cb_cls, - ...) -{ - struct TALER_EXCHANGE_Handle *exchange; - va_list ap; - enum TALER_EXCHANGE_Option opt; - TALER_LOG_DEBUG ("Connecting to the exchange (%s)\n", - url); - /* Disable 100 continue processing */ - GNUNET_break (GNUNET_OK == - GNUNET_CURL_append_header (ctx, - "Expect:")); - exchange = GNUNET_new (struct TALER_EXCHANGE_Handle); - exchange->ctx = ctx; - exchange->url = GNUNET_strdup (url); - exchange->cert_cb = cert_cb; - exchange->cert_cb_cls = cert_cb_cls; - exchange->retry_task = GNUNET_SCHEDULER_add_now (&request_keys, - exchange); - va_start (ap, cert_cb_cls); - while (TALER_EXCHANGE_OPTION_END != - (opt = va_arg (ap, int))) + global_fees = json_array (); + GNUNET_assert (NULL != global_fees); + for (unsigned int i = 0; i<kd->num_global_fees; i++) { - switch (opt) - { - case TALER_EXCHANGE_OPTION_END: - GNUNET_assert (0); - break; - case TALER_EXCHANGE_OPTION_DATA: - { - const json_t *data = va_arg (ap, const json_t *); + const struct TALER_EXCHANGE_GlobalFee *gf + = &kd->global_fees[i]; - deserialize_data (exchange, - data); - break; - } - default: - GNUNET_assert (0); - break; - } - } - va_end (ap); - return exchange; -} - - -/** - * Compute the network timeout for the next request to /keys. - * - * @param exchange the exchange handle - * @returns the timeout in seconds (for use by CURL) - */ -static long -get_keys_timeout_seconds (struct TALER_EXCHANGE_Handle *exchange) -{ - unsigned int kec; - - /* if retry counter >= 8, do not bother to go further, we - stop the exponential back-off at 128 anyway. */ - kec = GNUNET_MIN (7, - exchange->keys_error_count); - return GNUNET_MIN (120, - 5 + (1L << kec)); -} - - -/** - * Initiate download of /keys from the exchange. - * - * @param cls exchange where to download /keys from - */ -static void -request_keys (void *cls) -{ - struct TALER_EXCHANGE_Handle *exchange = cls; - struct KeysRequest *kr; - CURL *eh; - char url[200] = "/keys?"; - - exchange->retry_task = NULL; - GNUNET_assert (NULL == exchange->kr); - kr = GNUNET_new (struct KeysRequest); - kr->exchange = exchange; - - if (GNUNET_YES == TEAH_handle_is_ready (exchange)) - { - TALER_LOG_DEBUG ("Last DK issue date (before GETting /keys): %s\n", - GNUNET_STRINGS_absolute_time_to_string ( - exchange->key_data.last_denom_issue_date)); - sprintf (&url[strlen (url)], - "last_issue_date=%llu&", - (unsigned long - long) exchange->key_data.last_denom_issue_date.abs_value_us - / 1000000LLU); + if (GNUNET_TIME_absolute_is_past (gf->end_date.abs_time)) + continue; + GNUNET_assert ( + 0 == + json_array_append_new ( + global_fees, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("start_date", + gf->start_date), + GNUNET_JSON_pack_timestamp ("end_date", + gf->end_date), + TALER_JSON_PACK_GLOBAL_FEES (&gf->fees), + GNUNET_JSON_pack_time_rel ("history_expiration", + gf->history_expiration), + GNUNET_JSON_pack_time_rel ("purse_timeout", + gf->purse_timeout), + GNUNET_JSON_pack_uint64 ("purse_account_limit", + gf->purse_account_limit), + GNUNET_JSON_pack_data_auto ("master_sig", + &gf->master_sig)))); } - /* Clean the last '&'/'?' sign that we optimistically put. */ - url[strlen (url) - 1] = '\0'; - kr->url = TEAH_path_to_url (exchange, - url); - if (NULL == kr->url) + accounts = json_array (); + GNUNET_assert (NULL != accounts); + for (unsigned int i = 0; i<kd->accounts_len; i++) { - struct TALER_EXCHANGE_HttpResponse hr = { - .ec = TALER_EC_GENERIC_CONFIGURATION_INVALID - }; - - GNUNET_free (kr); - exchange->keys_error_count++; - exchange->state = MHS_FAILED; - exchange->cert_cb (exchange->cert_cb_cls, - &hr, - NULL, - TALER_EXCHANGE_VC_PROTOCOL_ERROR); - return; + const struct TALER_EXCHANGE_WireAccount *acc + = &kd->accounts[i]; + json_t *credit_restrictions; + json_t *debit_restrictions; + + credit_restrictions + = ar_to_json (acc->credit_restrictions_length, + acc->credit_restrictions); + GNUNET_assert (NULL != credit_restrictions); + debit_restrictions + = ar_to_json (acc->debit_restrictions_length, + acc->debit_restrictions); + GNUNET_assert (NULL != debit_restrictions); + GNUNET_assert ( + 0 == + json_array_append_new ( + accounts, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("payto_uri", + acc->payto_uri), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("conversion_url", + acc->conversion_url)), + GNUNET_JSON_pack_int64 ("priority", + acc->priority), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("bank_label", + acc->bank_label)), + GNUNET_JSON_pack_array_steal ("debit_restrictions", + debit_restrictions), + GNUNET_JSON_pack_array_steal ("credit_restrictions", + credit_restrictions), + GNUNET_JSON_pack_data_auto ("master_sig", + &acc->master_sig)))); } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Requesting keys with URL `%s'.\n", - kr->url); - eh = TALER_EXCHANGE_curl_easy_get_ (kr->url); - if (NULL == eh) - { - GNUNET_free (kr->url); - GNUNET_free (kr); - exchange->retry_delay = EXCHANGE_LIB_BACKOFF (exchange->retry_delay); - exchange->retry_task = GNUNET_SCHEDULER_add_delayed (exchange->retry_delay, - &request_keys, - exchange); - return; - } - GNUNET_break (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_VERBOSE, - 0)); - GNUNET_break (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_TIMEOUT, - get_keys_timeout_seconds (exchange))); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_HEADERFUNCTION, - &header_cb)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_HEADERDATA, - kr)); - kr->job = GNUNET_CURL_job_add_with_ct_json (exchange->ctx, - eh, - &keys_completed_cb, - kr); - exchange->kr = kr; -} - - -/** - * Disconnect from the exchange - * - * @param exchange the exchange handle - */ -void -TALER_EXCHANGE_disconnect (struct TALER_EXCHANGE_Handle *exchange) -{ - struct TEAH_AuditorListEntry *ale; + "Serialized %u/%u wire accounts to JSON\n", + (unsigned int) json_array_size (accounts), + kd->accounts_len); - while (NULL != (ale = exchange->auditors_head)) + wire_fees = json_object (); + GNUNET_assert (NULL != wire_fees); + for (unsigned int i = 0; i<kd->fees_len; i++) { - struct TEAH_AuditorInteractionEntry *aie; - - while (NULL != (aie = ale->ai_head)) + const struct TALER_EXCHANGE_WireFeesByMethod *fbw + = &kd->fees[i]; + json_t *wf; + + wf = json_array (); + GNUNET_assert (NULL != wf); + for (struct TALER_EXCHANGE_WireAggregateFees *p = fbw->fees_head; + NULL != p; + p = p->next) { - GNUNET_assert (aie->ale == ale); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Not sending deposit confirmation to auditor `%s' due to exchange disconnect\n", - ale->auditor_url); - TALER_AUDITOR_deposit_confirmation_cancel (aie->dch); - GNUNET_CONTAINER_DLL_remove (ale->ai_head, - ale->ai_tail, - aie); - GNUNET_free (aie); + GNUNET_assert ( + 0 == + json_array_append_new ( + wf, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("wire_fee", + &p->fees.wire), + TALER_JSON_pack_amount ("closing_fee", + &p->fees.closing), + GNUNET_JSON_pack_timestamp ("start_date", + p->start_date), + GNUNET_JSON_pack_timestamp ("end_date", + p->end_date), + GNUNET_JSON_pack_data_auto ("sig", + &p->master_sig)))); } - GNUNET_CONTAINER_DLL_remove (exchange->auditors_head, - exchange->auditors_tail, - ale); - TALER_LOG_DEBUG ("Disconnecting the auditor `%s'\n", - ale->auditor_url); - TALER_AUDITOR_disconnect (ale->ah); - GNUNET_free (ale->auditor_url); - GNUNET_free (ale); - } - if (NULL != exchange->kr) - { - GNUNET_CURL_job_cancel (exchange->kr->job); - free_keys_request (exchange->kr); - exchange->kr = NULL; - } - free_key_data (&exchange->key_data); - if (NULL != exchange->key_data_raw) - { - json_decref (exchange->key_data_raw); - exchange->key_data_raw = NULL; + GNUNET_assert (0 == + json_object_set_new (wire_fees, + fbw->method, + wf)); } - if (NULL != exchange->retry_task) + + recoup = json_array (); + GNUNET_assert (NULL != recoup); + for (unsigned int i = 0; i<kd->num_denom_keys; i++) { - GNUNET_SCHEDULER_cancel (exchange->retry_task); - exchange->retry_task = NULL; + const struct TALER_EXCHANGE_DenomPublicKey *dk + = &kd->denom_keys[i]; + if (! dk->revoked) + continue; + GNUNET_assert (0 == + json_array_append_new ( + recoup, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &dk->h_key)))); } - GNUNET_free (exchange->url); - GNUNET_free (exchange); -} - - -/** - * Test if the given @a pub is a the current signing key from the exchange - * according to @a keys. - * - * @param keys the exchange's key set - * @param pub claimed current online signing key for the exchange - * @return #GNUNET_OK if @a pub is (according to /keys) a current signing key - */ -int -TALER_EXCHANGE_test_signing_key (const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_ExchangePublicKeyP *pub) -{ - struct GNUNET_TIME_Absolute now; - - /* we will check using a tolerance of 1h for the time */ - now = GNUNET_TIME_absolute_get (); - for (unsigned int i = 0; i<keys->num_sign_keys; i++) - if ( (keys->sign_keys[i].valid_from.abs_value_us <= now.abs_value_us + 60 - * 60 * 1000LL * 1000LL) && - (keys->sign_keys[i].valid_until.abs_value_us > now.abs_value_us - 60 - * 60 * 1000LL * 1000LL) && - (0 == GNUNET_memcmp (pub, - &keys->sign_keys[i].key)) ) - return GNUNET_OK; - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Signing key not valid at time %llu\n", - (unsigned long long) now.abs_value_us); - return GNUNET_SYSERR; -} - - -/** - * Get exchange's base URL. - * - * @param exchange exchange handle. - * @return the base URL from the handle. - */ -const char * -TALER_EXCHANGE_get_base_url (const struct TALER_EXCHANGE_Handle *exchange) -{ - return exchange->url; -} + wblwk = json_array (); + GNUNET_assert (NULL != wblwk); + for (unsigned int i = 0; i<kd->wblwk_length; i++) + { + const struct TALER_Amount *a = &kd->wallet_balance_limit_without_kyc[i]; -/** - * Obtain the denomination key details from the exchange. - * - * @param keys the exchange's key set - * @param pk public key of the denomination to lookup - * @return details about the given denomination key, NULL if the key is - * not found - */ -const struct TALER_EXCHANGE_DenomPublicKey * -TALER_EXCHANGE_get_denomination_key ( - const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_DenominationPublicKey *pk) -{ - for (unsigned int i = 0; i<keys->num_denom_keys; i++) - if (0 == GNUNET_CRYPTO_rsa_public_key_cmp (pk->rsa_public_key, - keys->denom_keys[i].key. - rsa_public_key)) - return &keys->denom_keys[i]; - return NULL; -} - - -/** - * Create a copy of a denomination public key. - * - * @param key key to copy - * @returns a copy, must be freed with #TALER_EXCHANGE_destroy_denomination_key - */ -struct TALER_EXCHANGE_DenomPublicKey * -TALER_EXCHANGE_copy_denomination_key ( - const struct TALER_EXCHANGE_DenomPublicKey *key) -{ - struct TALER_EXCHANGE_DenomPublicKey *copy; - - copy = GNUNET_new (struct TALER_EXCHANGE_DenomPublicKey); - *copy = *key; - copy->key.rsa_public_key = GNUNET_CRYPTO_rsa_public_key_dup ( - key->key.rsa_public_key); - - return copy; -} - - -/** - * Destroy a denomination public key. - * Should only be called with keys created by #TALER_EXCHANGE_copy_denomination_key. - * - * @param key key to destroy. - */ -void -TALER_EXCHANGE_destroy_denomination_key ( - struct TALER_EXCHANGE_DenomPublicKey *key) -{ - GNUNET_CRYPTO_rsa_public_key_free (key->key.rsa_public_key);; - GNUNET_free (key); -} - - -/** - * Obtain the denomination key details from the exchange. - * - * @param keys the exchange's key set - * @param hc hash of the public key of the denomination to lookup - * @return details about the given denomination key - */ -const struct TALER_EXCHANGE_DenomPublicKey * -TALER_EXCHANGE_get_denomination_key_by_hash ( - const struct TALER_EXCHANGE_Keys *keys, - const struct GNUNET_HashCode *hc) -{ - for (unsigned int i = 0; i<keys->num_denom_keys; i++) - if (0 == GNUNET_memcmp (hc, - &keys->denom_keys[i].h_key)) - return &keys->denom_keys[i]; - return NULL; -} - - -/** - * Obtain the keys from the exchange. - * - * @param exchange the exchange handle - * @return the exchange's key set - */ -const struct TALER_EXCHANGE_Keys * -TALER_EXCHANGE_get_keys (struct TALER_EXCHANGE_Handle *exchange) -{ - (void) TALER_EXCHANGE_check_keys_current (exchange, - TALER_EXCHANGE_CKF_NONE); - return &exchange->key_data; -} - + GNUNET_assert (0 == + json_array_append_new ( + wblwk, + TALER_JSON_from_amount (a))); + } -/** - * Obtain the keys from the exchange in the - * raw JSON format - * - * @param exchange the exchange handle - * @return the exchange's keys in raw JSON - */ -json_t * -TALER_EXCHANGE_get_keys_raw (struct TALER_EXCHANGE_Handle *exchange) -{ - (void) TALER_EXCHANGE_check_keys_current (exchange, - TALER_EXCHANGE_CKF_NONE); - return json_deep_copy (exchange->key_data_raw); + keys = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("version", + kd->version), + GNUNET_JSON_pack_string ("currency", + kd->currency), + GNUNET_JSON_pack_object_steal ("currency_specification", + TALER_CONFIG_currency_specs_to_json ( + &kd->cspec)), + TALER_JSON_pack_amount ("stefan_abs", + &kd->stefan_abs), + TALER_JSON_pack_amount ("stefan_log", + &kd->stefan_log), + GNUNET_JSON_pack_double ("stefan_lin", + kd->stefan_lin), + GNUNET_JSON_pack_string ("asset_type", + kd->asset_type), + GNUNET_JSON_pack_data_auto ("master_public_key", + &kd->master_pub), + GNUNET_JSON_pack_time_rel ("reserve_closing_delay", + kd->reserve_closing_delay), + GNUNET_JSON_pack_timestamp ("list_issue_date", + kd->list_issue_date), + GNUNET_JSON_pack_array_steal ("global_fees", + global_fees), + GNUNET_JSON_pack_array_steal ("signkeys", + signkeys), + GNUNET_JSON_pack_object_steal ("wire_fees", + wire_fees), + GNUNET_JSON_pack_array_steal ("accounts", + accounts), + GNUNET_JSON_pack_array_steal ("wads", + json_array ()), + GNUNET_JSON_pack_array_steal ("denominations", + denominations_by_group), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("recoup", + recoup)), + GNUNET_JSON_pack_array_steal ("auditors", + auditors), + GNUNET_JSON_pack_bool ("rewards_allowed", + kd->rewards_allowed), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("extensions", + kd->extensions)), + GNUNET_JSON_pack_allow_null ( + GNUNET_is_zero (&kd->extensions_sig) + ? GNUNET_JSON_pack_string ("dummy", + NULL) + : GNUNET_JSON_pack_data_auto ("extensions_sig", + &kd->extensions_sig)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("wallet_balance_limit_without_kyc", + wblwk)) + + ); + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("version", + EXCHANGE_SERIALIZATION_FORMAT_VERSION), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("expire", + kd->key_data_expiration)), + GNUNET_JSON_pack_string ("exchange_url", + kd->exchange_url), + GNUNET_JSON_pack_object_steal ("keys", + keys)); } diff --git a/src/lib/exchange_api_handle.h b/src/lib/exchange_api_handle.h index 1a7e8ee7b..7c01b9a9f 100644 --- a/src/lib/exchange_api_handle.h +++ b/src/lib/exchange_api_handle.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014, 2015 Taler Systems SA + Copyright (C) 2014, 2015, 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 @@ -19,183 +19,29 @@ * @brief Internal interface to the handle part of the exchange's HTTP API * @author Christian Grothoff */ -#include "platform.h" +#ifndef EXCHANGE_API_HANDLE_H +#define EXCHANGE_API_HANDLE_H + #include <gnunet/gnunet_curl_lib.h> #include "taler_auditor_service.h" #include "taler_exchange_service.h" -#include "taler_crypto_lib.h" +#include "taler_util.h" #include "taler_curl_lib.h" -/** - * Entry in DLL of auditors used by an exchange. - */ -struct TEAH_AuditorListEntry; - - -/** - * Entry in list of ongoing interactions with an auditor. - */ -struct TEAH_AuditorInteractionEntry -{ - /** - * DLL entry. - */ - struct TEAH_AuditorInteractionEntry *next; - - /** - * DLL entry. - */ - struct TEAH_AuditorInteractionEntry *prev; - - /** - * Which auditor is this action associated with? - */ - struct TEAH_AuditorListEntry *ale; - - /** - * Interaction state. - */ - struct TALER_AUDITOR_DepositConfirmationHandle *dch; -}; - -/** - * Stages of initialization for the `struct TALER_EXCHANGE_Handle` - */ -enum ExchangeHandleState -{ - /** - * Just allocated. - */ - MHS_INIT = 0, - - /** - * Obtained the exchange's certification data and keys. - */ - MHS_CERT = 1, - - /** - * Failed to initialize (fatal). - */ - MHS_FAILED = 2 -}; - - -/** - * Handle to the exchange - */ -struct TALER_EXCHANGE_Handle -{ - /** - * The context of this handle - */ - struct GNUNET_CURL_Context *ctx; - - /** - * The URL of the exchange (i.e. "http://exchange.taler.net/") - */ - char *url; - - /** - * Function to call with the exchange's certification data, - * NULL if this has already been done. - */ - TALER_EXCHANGE_CertificationCallback cert_cb; - - /** - * Closure to pass to @e cert_cb. - */ - void *cert_cb_cls; - - /** - * Data for the request to get the /keys of a exchange, - * NULL once we are past stage #MHS_INIT. - */ - struct KeysRequest *kr; - - /** - * Task for retrying /keys request. - */ - struct GNUNET_SCHEDULER_Task *retry_task; - - /** - * Raw key data of the exchange, only valid if - * @e handshake_complete is past stage #MHS_CERT. - */ - json_t *key_data_raw; - - /** - * Head of DLL of auditors of this exchange. - */ - struct TEAH_AuditorListEntry *auditors_head; - - /** - * Tail of DLL of auditors of this exchange. - */ - struct TEAH_AuditorListEntry *auditors_tail; - - /** - * Key data of the exchange, only valid if - * @e handshake_complete is past stage #MHS_CERT. - */ - struct TALER_EXCHANGE_Keys key_data; - - /** - * Retry /keys frequency. - */ - struct GNUNET_TIME_Relative retry_delay; - - /** - * When does @e key_data expire? - */ - struct GNUNET_TIME_Absolute key_data_expiration; - - /** - * Number of subsequent failed requests to /keys. - * - * Used to compute the CURL timeout for the request. - */ - unsigned int keys_error_count; - - /** - * Number of subsequent failed requests to /wire. - * - * Used to compute the CURL timeout for the request. - */ - unsigned int wire_error_count; - - /** - * Stage of the exchange's initialization routines. - */ - enum ExchangeHandleState state; - -}; - /** * Function called for each auditor to give us a chance to possibly * launch a deposit confirmation interaction. * * @param cls closure - * @param ah handle to the auditor + * @param auditor_url base URL of the auditor * @param auditor_pub public key of the auditor - * @return NULL if no deposit confirmation interaction was launched */ -typedef struct TEAH_AuditorInteractionEntry * -(*TEAH_AuditorCallback)(void *cls, - struct TALER_AUDITOR_Handle *ah, - const struct TALER_AuditorPublicKeyP *auditor_pub); - - -/** - * Signature of functions called with the result from our call to the - * auditor's /deposit-confirmation handler. - * - * @param cls closure of type `struct TEAH_AuditorInteractionEntry *` - * @param hr HTTP response - */ -void -TEAH_acc_confirmation_cb (void *cls, - const struct TALER_AUDITOR_HttpResponse *hr); +typedef void +(*TEAH_AuditorCallback)( + void *cls, + const char *auditor_url, + const struct TALER_AuditorPublicKeyP *auditor_pub); /** @@ -203,54 +49,16 @@ TEAH_acc_confirmation_cb (void *cls, * @a ac and giving it a chance to start a deposit * confirmation interaction. * - * @param h exchange to go over auditors for + * @param keys the keys to go over auditors for * @param ac function to call per auditor * @param ac_cls closure for @a ac */ void -TEAH_get_auditors_for_dc (struct TALER_EXCHANGE_Handle *h, - TEAH_AuditorCallback ac, - void *ac_cls); - +TEAH_get_auditors_for_dc ( + struct TALER_EXCHANGE_Keys *keys, + TEAH_AuditorCallback ac, + void *ac_cls); -/** - * Get the context of a exchange. - * - * @param h the exchange handle to query - * @return ctx context to execute jobs in - */ -struct GNUNET_CURL_Context * -TEAH_handle_to_context (struct TALER_EXCHANGE_Handle *h); - - -/** - * Check if the handle is ready to process requests. - * - * @param h the exchange handle to query - * @return #GNUNET_YES if we are ready, #GNUNET_NO if not - */ -int -TEAH_handle_is_ready (struct TALER_EXCHANGE_Handle *h); - -/** - * Check if the handle is ready to process requests. - * - * @param h the exchange handle to query - * @return #GNUNET_YES if we are ready, #GNUNET_NO if not - */ -int -TEAH_handle_is_ready (struct TALER_EXCHANGE_Handle *h); - - -/** - * Obtain the URL to use for an API request. - * - * @param h the exchange handle to query - * @param path Taler API path (i.e. "/reserve/withdraw") - * @return the full URL to use with cURL - */ -char * -TEAH_path_to_url (struct TALER_EXCHANGE_Handle *h, - const char *path); /* end of exchange_api_handle.h */ +#endif diff --git a/src/lib/exchange_api_kyc_check.c b/src/lib/exchange_api_kyc_check.c new file mode 100644 index 000000000..5d3b3792b --- /dev/null +++ b/src/lib/exchange_api_kyc_check.c @@ -0,0 +1,321 @@ +/* + This file is part of TALER + Copyright (C) 2021-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_kyc_check.c + * @brief Implementation of the /kyc-check request + * @author Christian Grothoff + */ +#include "platform.h" +#include <microhttpd.h> /* just for HTTP check codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 ``/kyc-check`` handle + */ +struct TALER_EXCHANGE_KycCheckHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Keys of the exchange. + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_KycStatusCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Hash of the payto:// URL that is being KYC'ed. + */ + struct TALER_PaytoHashP h_payto; + +}; + + +/** + * Function called when we're done processing the + * HTTP /kyc-check request. + * + * @param cls the `struct TALER_EXCHANGE_KycCheckHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_kyc_check_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_KycCheckHandle *kch = cls; + const json_t *j = response; + struct TALER_EXCHANGE_KycStatus ks = { + .http_status = (unsigned int) response_code + }; + + kch->job = NULL; + switch (response_code) + { + case 0: + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + const json_t *kyc_details; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &ks.details.ok.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &ks.details.ok.exchange_pub), + GNUNET_JSON_spec_timestamp ("now", + &ks.details.ok.timestamp), + GNUNET_JSON_spec_object_const ("kyc_details", + &kyc_details), + TALER_JSON_spec_aml_decision ("aml_status", + &ks.details.ok.aml_status), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ks.http_status = 0; + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + ks.details.ok.kyc_details = kyc_details; + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (kch->keys, + &ks.details.ok.exchange_pub)) + { + GNUNET_break_op (0); + ks.http_status = 0; + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + GNUNET_JSON_parse_free (spec); + break; + } + + if (GNUNET_OK != + TALER_exchange_online_account_setup_success_verify ( + &kch->h_payto, + ks.details.ok.kyc_details, + ks.details.ok.timestamp, + &ks.details.ok.exchange_pub, + &ks.details.ok.exchange_sig)) + { + GNUNET_break_op (0); + ks.http_status = 0; + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + GNUNET_JSON_parse_free (spec); + break; + } + kch->cb (kch->cb_cls, + &ks); + GNUNET_JSON_parse_free (spec); + TALER_EXCHANGE_kyc_check_cancel (kch); + return; + } + case MHD_HTTP_ACCEPTED: + { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_web_url ("kyc_url", + &ks.details.accepted.kyc_url), + TALER_JSON_spec_aml_decision ("aml_status", + &ks.details.accepted.aml_status), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ks.http_status = 0; + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + kch->cb (kch->cb_cls, + &ks); + GNUNET_JSON_parse_free (spec); + TALER_EXCHANGE_kyc_check_cancel (kch); + return; + } + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_BAD_REQUEST: + ks.ec = TALER_JSON_get_error_code (j); + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + ks.ec = TALER_JSON_get_error_code (j); + break; + case MHD_HTTP_NOT_FOUND: + ks.ec = TALER_JSON_get_error_code (j); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_aml_decision ( + "aml_status", + &ks.details.unavailable_for_legal_reasons.aml_status), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ks.http_status = 0; + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + kch->cb (kch->cb_cls, + &ks); + GNUNET_JSON_parse_free (spec); + TALER_EXCHANGE_kyc_check_cancel (kch); + return; + } + case MHD_HTTP_INTERNAL_SERVER_ERROR: + ks.ec = TALER_JSON_get_error_code (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + ks.ec = TALER_JSON_get_error_code (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange kyc_check\n", + (unsigned int) response_code, + (int) ks.ec); + break; + } + kch->cb (kch->cb_cls, + &ks); + TALER_EXCHANGE_kyc_check_cancel (kch); +} + + +struct TALER_EXCHANGE_KycCheckHandle * +TALER_EXCHANGE_kyc_check ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + uint64_t requirement_row, + const struct TALER_PaytoHashP *h_payto, + enum TALER_KYCLOGIC_KycUserType ut, + struct GNUNET_TIME_Relative timeout, + TALER_EXCHANGE_KycStatusCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_KycCheckHandle *kch; + CURL *eh; + char *arg_str; + + { + char payto_str[sizeof (*h_payto) * 2]; + char *end; + unsigned long long timeout_ms; + + end = GNUNET_STRINGS_data_to_string ( + h_payto, + sizeof (*h_payto), + payto_str, + sizeof (payto_str) - 1); + *end = '\0'; + timeout_ms = timeout.rel_value_us + / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us; + GNUNET_asprintf (&arg_str, + "kyc-check/%llu/%s/%s?timeout_ms=%llu", + (unsigned long long) requirement_row, + payto_str, + TALER_KYCLOGIC_kyc_user_type2s (ut), + timeout_ms); + } + kch = GNUNET_new (struct TALER_EXCHANGE_KycCheckHandle); + kch->h_payto = *h_payto; + kch->cb = cb; + kch->cb_cls = cb_cls; + kch->url = TALER_url_join (url, + arg_str, + NULL); + GNUNET_free (arg_str); + if (NULL == kch->url) + { + GNUNET_free (kch); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (kch->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (kch->url); + GNUNET_free (kch); + return NULL; + } + kch->keys = TALER_EXCHANGE_keys_incref (keys); + kch->job = GNUNET_CURL_job_add_with_ct_json (ctx, + eh, + &handle_kyc_check_finished, + kch); + return kch; +} + + +void +TALER_EXCHANGE_kyc_check_cancel (struct TALER_EXCHANGE_KycCheckHandle *kch) +{ + if (NULL != kch->job) + { + GNUNET_CURL_job_cancel (kch->job); + kch->job = NULL; + } + TALER_EXCHANGE_keys_decref (kch->keys); + GNUNET_free (kch->url); + GNUNET_free (kch); +} + + +/* end of exchange_api_kyc_check.c */ diff --git a/src/lib/exchange_api_kyc_proof.c b/src/lib/exchange_api_kyc_proof.c new file mode 100644 index 000000000..e7cc9c4cf --- /dev/null +++ b/src/lib/exchange_api_kyc_proof.c @@ -0,0 +1,217 @@ +/* + This file is part of TALER + Copyright (C) 2021, 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_kyc_proof.c + * @brief Implementation of the /kyc-proof request + * @author Christian Grothoff + */ +#include "platform.h" +#include <microhttpd.h> /* just for HTTP proof codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 ``/kyc-proof`` handle + */ +struct TALER_EXCHANGE_KycProofHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle to our CURL request. + */ + CURL *eh; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_KycProofCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + +}; + + +/** + * Function called when we're done processing the + * HTTP /kyc-proof request. + * + * @param cls the `struct TALER_EXCHANGE_KycProofHandle` + * @param response_code HTTP response code, 0 on error + * @param body response body + * @param body_size number of bytes in @a body + */ +static void +handle_kyc_proof_finished (void *cls, + long response_code, + const void *body, + size_t body_size) +{ + struct TALER_EXCHANGE_KycProofHandle *kph = cls; + struct TALER_EXCHANGE_KycProofResponse kpr = { + .http_status = (unsigned int) response_code + }; + + (void) body; + (void) body_size; + kph->job = NULL; + switch (response_code) + { + case 0: + break; + case MHD_HTTP_SEE_OTHER: + { + char *redirect_url; + + GNUNET_assert (CURLE_OK == + curl_easy_getinfo (kph->eh, + CURLINFO_REDIRECT_URL, + &redirect_url)); + kpr.details.found.redirect_url = redirect_url; + 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 */ + break; + case MHD_HTTP_UNAUTHORIZED: + break; + case MHD_HTTP_FORBIDDEN: + break; + case MHD_HTTP_NOT_FOUND: + break; + case MHD_HTTP_BAD_GATEWAY: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + case MHD_HTTP_GATEWAY_TIMEOUT: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u for exchange kyc_proof\n", + (unsigned int) response_code); + break; + } + kph->cb (kph->cb_cls, + &kpr); + TALER_EXCHANGE_kyc_proof_cancel (kph); +} + + +struct TALER_EXCHANGE_KycProofHandle * +TALER_EXCHANGE_kyc_proof ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_PaytoHashP *h_payto, + const char *logic, + const char *args, + TALER_EXCHANGE_KycProofCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_KycProofHandle *kph; + char *arg_str; + + if (NULL == args) + args = ""; + else + GNUNET_assert (args[0] == '&'); + { + char hstr[sizeof (struct TALER_PaytoHashP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string (h_payto, + sizeof (*h_payto), + hstr, + sizeof (hstr)); + *end = '\0'; + GNUNET_asprintf (&arg_str, + "kyc-proof/%s?state=%s%s", + logic, + hstr, + args); + } + kph = GNUNET_new (struct TALER_EXCHANGE_KycProofHandle); + kph->cb = cb; + kph->cb_cls = cb_cls; + kph->url = TALER_url_join (url, + arg_str, + NULL); + GNUNET_free (arg_str); + if (NULL == kph->url) + { + GNUNET_free (kph); + return NULL; + } + kph->eh = TALER_EXCHANGE_curl_easy_get_ (kph->url); + if (NULL == kph->eh) + { + GNUNET_break (0); + GNUNET_free (kph->url); + GNUNET_free (kph); + return NULL; + } + /* disable location following, we want to learn the + result of a 303 redirect! */ + GNUNET_assert (CURLE_OK == + curl_easy_setopt (kph->eh, + CURLOPT_FOLLOWLOCATION, + 0L)); + kph->job = GNUNET_CURL_job_add_raw (ctx, + kph->eh, + NULL, + &handle_kyc_proof_finished, + kph); + return kph; +} + + +void +TALER_EXCHANGE_kyc_proof_cancel (struct TALER_EXCHANGE_KycProofHandle *kph) +{ + if (NULL != kph->job) + { + GNUNET_CURL_job_cancel (kph->job); + kph->job = NULL; + } + GNUNET_free (kph->url); + GNUNET_free (kph); +} + + +/* end of exchange_api_kyc_proof.c */ diff --git a/src/lib/exchange_api_kyc_wallet.c b/src/lib/exchange_api_kyc_wallet.c new file mode 100644 index 000000000..7197694ae --- /dev/null +++ b/src/lib/exchange_api_kyc_wallet.c @@ -0,0 +1,230 @@ +/* + This file is part of TALER + Copyright (C) 2021 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_kyc_wallet.c + * @brief Implementation of the /kyc-wallet request + * @author Christian Grothoff + */ +#include "platform.h" +#include <microhttpd.h> /* just for HTTP wallet codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 ``/kyc-wallet`` handle + */ +struct TALER_EXCHANGE_KycWalletHandle +{ + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_KycWalletCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + +}; + + +/** + * Function called when we're done processing the + * HTTP /kyc-wallet request. + * + * @param cls the `struct TALER_EXCHANGE_KycWalletHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_kyc_wallet_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_KycWalletHandle *kwh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_WalletKycResponse ks = { + .http_status = (unsigned int) response_code + }; + + kwh->job = NULL; + switch (response_code) + { + case 0: + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_BAD_REQUEST: + ks.ec = TALER_JSON_get_error_code (j); + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + ks.ec = TALER_JSON_get_error_code (j); + break; + case MHD_HTTP_NOT_FOUND: + ks.ec = TALER_JSON_get_error_code (j); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ( + "h_payto", + &ks.details.unavailable_for_legal_reasons.h_payto), + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &ks.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ks.http_status = 0; + ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + break; + } + case MHD_HTTP_INTERNAL_SERVER_ERROR: + ks.ec = TALER_JSON_get_error_code (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + ks.ec = TALER_JSON_get_error_code (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange /kyc-wallet\n", + (unsigned int) response_code, + (int) ks.ec); + break; + } + kwh->cb (kwh->cb_cls, + &ks); + TALER_EXCHANGE_kyc_wallet_cancel (kwh); +} + + +struct TALER_EXCHANGE_KycWalletHandle * +TALER_EXCHANGE_kyc_wallet ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_Amount *balance, + TALER_EXCHANGE_KycWalletCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_KycWalletHandle *kwh; + CURL *eh; + json_t *req; + struct TALER_ReservePublicKeyP reserve_pub; + struct TALER_ReserveSignatureP reserve_sig; + + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &reserve_pub.eddsa_pub); + TALER_wallet_account_setup_sign (reserve_priv, + balance, + &reserve_sig); + req = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("balance", + balance), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &reserve_pub), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &reserve_sig)); + GNUNET_assert (NULL != req); + kwh = GNUNET_new (struct TALER_EXCHANGE_KycWalletHandle); + kwh->cb = cb; + kwh->cb_cls = cb_cls; + kwh->url = TALER_url_join (url, + "kyc-wallet", + NULL); + if (NULL == kwh->url) + { + json_decref (req); + GNUNET_free (kwh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (kwh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&kwh->ctx, + eh, + req)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (req); + GNUNET_free (kwh->url); + GNUNET_free (kwh); + return NULL; + } + json_decref (req); + kwh->job = GNUNET_CURL_job_add2 (ctx, + eh, + kwh->ctx.headers, + &handle_kyc_wallet_finished, + kwh); + return kwh; +} + + +void +TALER_EXCHANGE_kyc_wallet_cancel (struct TALER_EXCHANGE_KycWalletHandle *kwh) +{ + if (NULL != kwh->job) + { + GNUNET_CURL_job_cancel (kwh->job); + kwh->job = NULL; + } + GNUNET_free (kwh->url); + TALER_curl_easy_post_finished (&kwh->ctx); + GNUNET_free (kwh); +} + + +/* end of exchange_api_kyc_wallet.c */ diff --git a/src/lib/exchange_api_link.c b/src/lib/exchange_api_link.c index 7f29b3b8c..4b1adc723 100644 --- a/src/lib/exchange_api_link.c +++ b/src/lib/exchange_api_link.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2020 Taler Systems SA + Copyright (C) 2015-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 @@ -37,11 +37,6 @@ struct TALER_EXCHANGE_LinkHandle { /** - * The connection to exchange this request handle will use - */ - struct TALER_EXCHANGE_Handle *exchange; - - /** * The url for this request. */ char *url; @@ -66,6 +61,12 @@ struct TALER_EXCHANGE_LinkHandle */ struct TALER_CoinSpendPrivateKeyP coin_priv; + /** + * Age commitment and proof of the original coin, might be NULL. + * Required to derive the new age commitment and proof. + */ + const struct TALER_AgeCommitmentProof *age_commitment_proof; + }; @@ -75,33 +76,45 @@ struct TALER_EXCHANGE_LinkHandle * * @param lh link handle * @param json json reply with the data for one coin - * @param coin_num number of the coin * @param trans_pub our transfer public key - * @param[out] coin_priv where to return private coin key - * @param[out] sig where to return private coin signature - * @param[out] pub where to return the public key for the coin + * @param[out] lci where to return coin details * @return #GNUNET_OK on success, #GNUNET_SYSERR on error */ -static int +static enum GNUNET_GenericReturnValue parse_link_coin (const struct TALER_EXCHANGE_LinkHandle *lh, const json_t *json, - uint32_t coin_num, const struct TALER_TransferPublicKeyP *trans_pub, - struct TALER_CoinSpendPrivateKeyP *coin_priv, - struct TALER_DenominationSignature *sig, - struct TALER_DenominationPublicKey *pub) + struct TALER_EXCHANGE_LinkedCoinInfo *lci) { - struct GNUNET_CRYPTO_RsaSignature *bsig; - struct GNUNET_CRYPTO_RsaPublicKey *rpub; + struct TALER_BlindedDenominationSignature bsig; + struct TALER_DenominationPublicKey rpub; struct TALER_CoinSpendSignatureP link_sig; + union GNUNET_CRYPTO_BlindingSecretP bks; + struct TALER_ExchangeWithdrawValues alg_values; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + bool no_nonce; + uint32_t coin_idx; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_rsa_public_key ("denom_pub", &rpub), - GNUNET_JSON_spec_rsa_signature ("ev_sig", &bsig), - GNUNET_JSON_spec_fixed_auto ("link_sig", &link_sig), + TALER_JSON_spec_denom_pub ("denom_pub", + &rpub), + TALER_JSON_spec_blinded_denom_sig ("ev_sig", + &bsig), + TALER_JSON_spec_exchange_withdraw_values ("ewv", + &alg_values), + GNUNET_JSON_spec_fixed_auto ("link_sig", + &link_sig), + GNUNET_JSON_spec_uint32 ("coin_idx", + &coin_idx), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("cs_nonce", + &nonce), + &no_nonce), GNUNET_JSON_spec_end () }; struct TALER_TransferSecretP secret; - struct TALER_PlanchetSecretsP fc; + struct TALER_PlanchetDetail pd; + struct TALER_CoinPubHashP c_hash; + struct TALER_AgeCommitmentHash *pah = NULL; /* parse reply */ if (GNUNET_OK != @@ -115,53 +128,94 @@ parse_link_coin (const struct TALER_EXCHANGE_LinkHandle *lh, TALER_link_recover_transfer_secret (trans_pub, &lh->coin_priv, &secret); - TALER_planchet_setup_refresh (&secret, - coin_num, - &fc); + TALER_transfer_secret_to_planchet_secret (&secret, + coin_idx, + &lci->ps); + TALER_planchet_setup_coin_priv (&lci->ps, + &alg_values, + &lci->coin_priv); + TALER_planchet_blinding_secret_create (&lci->ps, + &alg_values, + &bks); + + lci->has_age_commitment = false; + + /* Derive the age commitment and calculate the hash */ + if (NULL != lh->age_commitment_proof) + { + GNUNET_assert (GNUNET_OK == + TALER_age_commitment_derive ( + lh->age_commitment_proof, + &secret.key, + &lci->age_commitment_proof)); + + TALER_age_commitment_hash ( + &lci->age_commitment_proof.commitment, + &lci->h_age_commitment); + + lci->has_age_commitment = true; + pah = &lci->h_age_commitment; + } + + if (GNUNET_OK != + TALER_planchet_prepare ( + &rpub, + &alg_values, + &bks, + no_nonce + ? NULL + : &nonce, + &lci->coin_priv, + pah, + &c_hash, + &pd)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; + } /* extract coin and signature */ - *coin_priv = fc.coin_priv; - sig->rsa_signature - = TALER_rsa_unblind (bsig, - &fc.blinding_key.bks, - rpub); + if (GNUNET_OK != + TALER_denom_sig_unblind (&lci->sig, + &bsig, + &bks, + &c_hash, + &alg_values, + &rpub)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } /* verify link_sig */ { - struct TALER_PlanchetDetail pd; - struct GNUNET_HashCode c_hash; struct TALER_CoinSpendPublicKeyP old_coin_pub; + struct TALER_BlindedCoinHashP coin_envelope_hash; GNUNET_CRYPTO_eddsa_key_get_public (&lh->coin_priv.eddsa_priv, &old_coin_pub.eddsa_pub); - pub->rsa_public_key = rpub; - if (GNUNET_OK != - TALER_planchet_prepare (pub, - &fc, - &c_hash, - &pd)) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return GNUNET_SYSERR; - } + + TALER_coin_ev_hash (&pd.blinded_planchet, + &pd.denom_pub_hash, + &coin_envelope_hash); if (GNUNET_OK != TALER_wallet_link_verify (&pd.denom_pub_hash, trans_pub, - pd.coin_ev, - pd.coin_ev_size, + &coin_envelope_hash, &old_coin_pub, &link_sig)) { GNUNET_break_op (0); - GNUNET_free (pd.coin_ev); + TALER_blinded_planchet_free (&pd.blinded_planchet); GNUNET_JSON_parse_free (spec); return GNUNET_SYSERR; } - GNUNET_free (pd.coin_ev); + TALER_blinded_planchet_free (&pd.blinded_planchet); } /* clean up */ - pub->rsa_public_key = GNUNET_CRYPTO_rsa_public_key_dup (rpub); + TALER_denom_pub_copy (&lci->pub, + &rpub); GNUNET_JSON_parse_free (spec); return GNUNET_OK; } @@ -175,16 +229,16 @@ parse_link_coin (const struct TALER_EXCHANGE_LinkHandle *lh, * @param json json reply with the data for one coin * @return #GNUNET_OK on success, #GNUNET_SYSERR on error */ -static int +static enum GNUNET_GenericReturnValue parse_link_ok (struct TALER_EXCHANGE_LinkHandle *lh, const json_t *json) { unsigned int session; unsigned int num_coins; int ret; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = json, - .http_status = MHD_HTTP_OK + struct TALER_EXCHANGE_LinkResult lr = { + .hr.reply = json, + .hr.http_status = MHD_HTTP_OK }; if (! json_is_array (json)) @@ -206,9 +260,10 @@ parse_link_ok (struct TALER_EXCHANGE_LinkHandle *lh, whilst 'i' and 'session' track the 2d array. */// for (session = 0; session<json_array_size (json); session++) { - json_t *jsona; + const json_t *jsona; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("new_coins", &jsona), + GNUNET_JSON_spec_array_const ("new_coins", + &jsona), GNUNET_JSON_spec_end () }; @@ -221,36 +276,25 @@ parse_link_ok (struct TALER_EXCHANGE_LinkHandle *lh, GNUNET_break_op (0); return GNUNET_SYSERR; } - if (! json_is_array (jsona)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return GNUNET_SYSERR; - } - /* count all coins over all sessions */ num_coins += json_array_size (jsona); - GNUNET_JSON_parse_free (spec); } /* Now that we know how big the 1d array is, allocate and fill it. */ { unsigned int off_coin; /* index into 1d array */ unsigned int i; - struct TALER_CoinSpendPrivateKeyP coin_privs[GNUNET_NZL (num_coins)]; - struct TALER_DenominationSignature sigs[GNUNET_NZL (num_coins)]; - struct TALER_DenominationPublicKey pubs[GNUNET_NZL (num_coins)]; + struct TALER_EXCHANGE_LinkedCoinInfo lcis[GNUNET_NZL (num_coins)]; - memset (sigs, 0, sizeof (sigs)); - memset (pubs, 0, sizeof (pubs)); + memset (lcis, 0, sizeof (lcis)); off_coin = 0; for (session = 0; session<json_array_size (json); session++) { - json_t *jsona; + const json_t *jsona; struct TALER_TransferPublicKeyP trans_pub; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("new_coins", - &jsona), + GNUNET_JSON_spec_array_const ("new_coins", + &jsona), GNUNET_JSON_spec_fixed_auto ("transfer_pub", &trans_pub), GNUNET_JSON_spec_end () @@ -265,26 +309,20 @@ parse_link_ok (struct TALER_EXCHANGE_LinkHandle *lh, GNUNET_break_op (0); return GNUNET_SYSERR; } - if (! json_is_array (jsona)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return GNUNET_SYSERR; - } /* decode all coins */ for (i = 0; i<json_array_size (jsona); i++) { + struct TALER_EXCHANGE_LinkedCoinInfo *lci; + + lci = &lcis[i + off_coin]; GNUNET_assert (i + off_coin < num_coins); if (GNUNET_OK != parse_link_coin (lh, json_array_get (jsona, i), - i, &trans_pub, - &coin_privs[i + off_coin], - &sigs[i + off_coin], - &pubs[i + off_coin])) + lci)) { GNUNET_break_op (0); break; @@ -296,20 +334,16 @@ parse_link_ok (struct TALER_EXCHANGE_LinkHandle *lh, { GNUNET_break_op (0); ret = GNUNET_SYSERR; - GNUNET_JSON_parse_free (spec); break; } - GNUNET_JSON_parse_free (spec); } /* end of for (session) */ if (off_coin == num_coins) { + lr.details.ok.num_coins = num_coins; + lr.details.ok.coins = lcis; lh->link_cb (lh->link_cb_cls, - &hr, - num_coins, - coin_privs, - sigs, - pubs); + &lr); lh->link_cb = NULL; ret = GNUNET_OK; } @@ -323,10 +357,10 @@ parse_link_ok (struct TALER_EXCHANGE_LinkHandle *lh, GNUNET_assert (off_coin <= num_coins); for (i = 0; i<off_coin; i++) { - if (NULL != sigs[i].rsa_signature) - GNUNET_CRYPTO_rsa_signature_free (sigs[i].rsa_signature); - if (NULL != pubs[i].rsa_public_key) - GNUNET_CRYPTO_rsa_public_key_free (pubs[i].rsa_public_key); + TALER_denom_sig_free (&lcis[i].sig); + TALER_denom_pub_free (&lcis[i].pub); + if (lcis[i].has_age_commitment) + TALER_age_commitment_proof_free (&lcis[i].age_commitment_proof); } } return ret; @@ -348,16 +382,16 @@ handle_link_finished (void *cls, { struct TALER_EXCHANGE_LinkHandle *lh = cls; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_LinkResult lr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; lh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + lr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: if (GNUNET_OK != @@ -365,85 +399,63 @@ handle_link_finished (void *cls, j)) { GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + lr.hr.http_status = 0; + lr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } GNUNET_assert (NULL == lh->link_cb); TALER_EXCHANGE_link_cancel (lh); return; case MHD_HTTP_BAD_REQUEST: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); /* This should never happen, either us or the exchange is buggy (or API version conflict); just pass JSON reply to the application */ break; case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); /* Nothing really to verify, exchange says this coin was not melted; we should pass the JSON reply to the application */ break; case MHD_HTTP_INTERNAL_SERVER_ERROR: - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); /* Server had an internal issue; we should retry, but this API leaves this to the application */ 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); + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange link\n", (unsigned int) response_code, - (int) hr.ec); + (int) lr.hr.ec); break; } if (NULL != lh->link_cb) lh->link_cb (lh->link_cb_cls, - &hr, - 0, - NULL, - NULL, - NULL); + &lr); TALER_EXCHANGE_link_cancel (lh); } -/** - * Submit a link request to the exchange and get the exchange's response. - * - * This API is typically not used by anyone, it is more a threat against those - * trying to receive a funds transfer by abusing the refresh protocol. - * - * @param exchange the exchange handle; the exchange must be ready to operate - * @param coin_priv private key to request link data for - * @param link_cb the callback to call with the useful result of the - * refresh operation the @a coin_priv was involved in (if any) - * @param link_cb_cls closure for @a link_cb - * @return a handle for this request - */ struct TALER_EXCHANGE_LinkHandle * -TALER_EXCHANGE_link (struct TALER_EXCHANGE_Handle *exchange, - const struct TALER_CoinSpendPrivateKeyP *coin_priv, - TALER_EXCHANGE_LinkCallback link_cb, - void *link_cb_cls) +TALER_EXCHANGE_link ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_CoinSpendPrivateKeyP *coin_priv, + const struct TALER_AgeCommitmentProof *age_commitment_proof, + TALER_EXCHANGE_LinkCallback link_cb, + void *link_cb_cls) { struct TALER_EXCHANGE_LinkHandle *lh; CURL *eh; - struct GNUNET_CURL_Context *ctx; struct TALER_CoinSpendPublicKeyP coin_pub; char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32]; - if (GNUNET_YES != - TEAH_handle_is_ready (exchange)) - { - GNUNET_break (0); - return NULL; - } - GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv, &coin_pub.eddsa_pub); { @@ -458,16 +470,17 @@ TALER_EXCHANGE_link (struct TALER_EXCHANGE_Handle *exchange, *end = '\0'; GNUNET_snprintf (arg_str, sizeof (arg_str), - "/coins/%s/link", + "coins/%s/link", pub_str); } lh = GNUNET_new (struct TALER_EXCHANGE_LinkHandle); - lh->exchange = exchange; lh->link_cb = link_cb; lh->link_cb_cls = link_cb_cls; lh->coin_priv = *coin_priv; - lh->url = TEAH_path_to_url (exchange, - arg_str); + lh->age_commitment_proof = age_commitment_proof; + lh->url = TALER_url_join (url, + arg_str, + NULL); if (NULL == lh->url) { GNUNET_free (lh); @@ -481,7 +494,6 @@ TALER_EXCHANGE_link (struct TALER_EXCHANGE_Handle *exchange, GNUNET_free (lh); return NULL; } - ctx = TEAH_handle_to_context (exchange); lh->job = GNUNET_CURL_job_add_with_ct_json (ctx, eh, &handle_link_finished, @@ -490,12 +502,6 @@ TALER_EXCHANGE_link (struct TALER_EXCHANGE_Handle *exchange, } -/** - * Cancel a link request. This function cannot be used - * on a request handle if the callback was already invoked. - * - * @param lh the link handle - */ void TALER_EXCHANGE_link_cancel (struct TALER_EXCHANGE_LinkHandle *lh) { @@ -504,6 +510,7 @@ TALER_EXCHANGE_link_cancel (struct TALER_EXCHANGE_LinkHandle *lh) GNUNET_CURL_job_cancel (lh->job); lh->job = NULL; } + GNUNET_free (lh->url); GNUNET_free (lh); } diff --git a/src/lib/exchange_api_lookup_aml_decision.c b/src/lib/exchange_api_lookup_aml_decision.c new file mode 100644 index 000000000..501b9d185 --- /dev/null +++ b/src/lib/exchange_api_lookup_aml_decision.c @@ -0,0 +1,417 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_lookup_aml_decision.c + * @brief Implementation of the /aml/$OFFICER_PUB/decision request + * @author Christian Grothoff + */ +#include "platform.h" +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 /coins/$COIN_PUB/link Handle + */ +struct TALER_EXCHANGE_LookupAmlDecision +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_LookupAmlDecisionCallback decision_cb; + + /** + * Closure for @e cb. + */ + void *decision_cb_cls; + + /** + * HTTP headers for the job. + */ + struct curl_slist *job_headers; +}; + + +/** + * Parse AML decision history. + * + * @param aml_history JSON array with AML history + * @param[out] aml_history_ar where to write the result + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_aml_history (const json_t *aml_history, + struct TALER_EXCHANGE_AmlDecisionDetail *aml_history_ar) +{ + json_t *obj; + size_t idx; + + json_array_foreach (aml_history, idx, obj) + { + struct TALER_EXCHANGE_AmlDecisionDetail *aml = &aml_history_ar[idx]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("decision_time", + &aml->decision_time), + GNUNET_JSON_spec_string ("justification", + &aml->justification), + TALER_JSON_spec_amount_any ("new_threshold", + &aml->new_threshold), + TALER_JSON_spec_aml_decision ("new_state", + &aml->new_state), + GNUNET_JSON_spec_fixed_auto ("decider_pub", + &aml->decider_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (obj, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Parse KYC response array. + * + * @param kyc_attributes JSON array with KYC details + * @param[out] kyc_attributes_ar where to write the result + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_kyc_attributes (const json_t *kyc_attributes, + struct TALER_EXCHANGE_KycHistoryDetail *kyc_attributes_ar) +{ + json_t *obj; + size_t idx; + + json_array_foreach (kyc_attributes, idx, obj) + { + struct TALER_EXCHANGE_KycHistoryDetail *kyc = &kyc_attributes_ar[idx]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("collection_time", + &kyc->collection_time), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("attributes", + &kyc->attributes), + NULL), + GNUNET_JSON_spec_string ("provider_section", + &kyc->provider_section), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (obj, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Parse the provided decision data from the "200 OK" response. + * + * @param[in,out] lh handle (callback may be zero'ed out) + * @param json json reply with the data for one coin + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static enum GNUNET_GenericReturnValue +parse_decision_ok (struct TALER_EXCHANGE_LookupAmlDecision *lh, + const json_t *json) +{ + struct TALER_EXCHANGE_AmlDecisionResponse lr = { + .hr.reply = json, + .hr.http_status = MHD_HTTP_OK + }; + const json_t *aml_history; + const json_t *kyc_attributes; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("aml_history", + &aml_history), + GNUNET_JSON_spec_array_const ("kyc_attributes", + &kyc_attributes), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + lr.details.ok.aml_history_length = json_array_size (aml_history); + lr.details.ok.kyc_attributes_length = json_array_size (kyc_attributes); + { + struct TALER_EXCHANGE_AmlDecisionDetail aml_history_ar[ + GNUNET_NZL (lr.details.ok.aml_history_length)]; + struct TALER_EXCHANGE_KycHistoryDetail kyc_attributes_ar[ + GNUNET_NZL (lr.details.ok.kyc_attributes_length)]; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + + memset (aml_history_ar, + 0, + sizeof (aml_history_ar)); + memset (kyc_attributes_ar, + 0, + sizeof (kyc_attributes_ar)); + lr.details.ok.aml_history = aml_history_ar; + lr.details.ok.kyc_attributes = kyc_attributes_ar; + ret = parse_aml_history (aml_history, + aml_history_ar); + if (GNUNET_OK == ret) + ret = parse_kyc_attributes (kyc_attributes, + kyc_attributes_ar); + if (GNUNET_OK == ret) + { + lh->decision_cb (lh->decision_cb_cls, + &lr); + lh->decision_cb = NULL; + } + return ret; + } +} + + +/** + * Function called when we're done processing the + * HTTP /aml/$OFFICER_PUB/decision request. + * + * @param cls the `struct TALER_EXCHANGE_LookupAmlDecision` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_lookup_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_LookupAmlDecision *lh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_AmlDecisionResponse lr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + lh->job = NULL; + switch (response_code) + { + case 0: + lr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + parse_decision_ok (lh, + j)) + { + GNUNET_break_op (0); + lr.hr.http_status = 0; + lr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == lh->decision_cb); + TALER_EXCHANGE_lookup_aml_decision_cancel (lh); + return; + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_BAD_REQUEST: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, exchange says this coin was not melted; we + should pass the JSON reply to the application */ + break; + case MHD_HTTP_NOT_FOUND: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, exchange says this coin was not melted; we + should pass the JSON reply to the application */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange lookup AML decision\n", + (unsigned int) response_code, + (int) lr.hr.ec); + break; + } + if (NULL != lh->decision_cb) + lh->decision_cb (lh->decision_cb_cls, + &lr); + TALER_EXCHANGE_lookup_aml_decision_cancel (lh); +} + + +struct TALER_EXCHANGE_LookupAmlDecision * +TALER_EXCHANGE_lookup_aml_decision ( + struct GNUNET_CURL_Context *ctx, + const char *exchange_url, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_AmlOfficerPrivateKeyP *officer_priv, + bool history, + TALER_EXCHANGE_LookupAmlDecisionCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_LookupAmlDecision *lh; + CURL *eh; + struct TALER_AmlOfficerPublicKeyP officer_pub; + struct TALER_AmlOfficerSignatureP officer_sig; + char arg_str[sizeof (officer_pub) * 2 + + sizeof (*h_payto) * 2 + 32]; + + GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv, + &officer_pub.eddsa_pub); + TALER_officer_aml_query_sign (officer_priv, + &officer_sig); + { + char pub_str[sizeof (officer_pub) * 2]; + char pt_str[sizeof (*h_payto) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &officer_pub, + sizeof (officer_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + end = GNUNET_STRINGS_data_to_string ( + h_payto, + sizeof (*h_payto), + pt_str, + sizeof (pt_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "aml/%s/decision/%s", + pub_str, + pt_str); + } + lh = GNUNET_new (struct TALER_EXCHANGE_LookupAmlDecision); + lh->decision_cb = cb; + lh->decision_cb_cls = cb_cls; + lh->url = TALER_url_join (exchange_url, + arg_str, + "history", + history + ? "true" + : NULL, + NULL); + if (NULL == lh->url) + { + GNUNET_free (lh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (lh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (lh->url); + GNUNET_free (lh); + return NULL; + } + { + char *hdr; + char sig_str[sizeof (officer_sig) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &officer_sig, + sizeof (officer_sig), + sig_str, + sizeof (sig_str)); + *end = '\0'; + + GNUNET_asprintf (&hdr, + "%s: %s", + TALER_AML_OFFICER_SIGNATURE_HEADER, + sig_str); + lh->job_headers = curl_slist_append (NULL, + hdr); + GNUNET_free (hdr); + lh->job_headers = curl_slist_append (lh->job_headers, + "Content-type: application/json"); + lh->job = GNUNET_CURL_job_add2 (ctx, + eh, + lh->job_headers, + &handle_lookup_finished, + lh); + } + return lh; +} + + +void +TALER_EXCHANGE_lookup_aml_decision_cancel ( + struct TALER_EXCHANGE_LookupAmlDecision *lh) +{ + if (NULL != lh->job) + { + GNUNET_CURL_job_cancel (lh->job); + lh->job = NULL; + } + curl_slist_free_all (lh->job_headers); + GNUNET_free (lh->url); + GNUNET_free (lh); +} + + +/* end of exchange_api_lookup_aml_decision.c */ diff --git a/src/lib/exchange_api_lookup_aml_decisions.c b/src/lib/exchange_api_lookup_aml_decisions.c new file mode 100644 index 000000000..bb3c18b68 --- /dev/null +++ b/src/lib/exchange_api_lookup_aml_decisions.c @@ -0,0 +1,376 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_lookup_aml_decisions.c + * @brief Implementation of the /aml/$OFFICER_PUB/decisions request + * @author Christian Grothoff + */ +#include "platform.h" +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 /coins/$COIN_PUB/link Handle + */ +struct TALER_EXCHANGE_LookupAmlDecisions +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_LookupAmlDecisionsCallback decisions_cb; + + /** + * Closure for @e cb. + */ + void *decisions_cb_cls; + + /** + * HTTP headers for the job. + */ + struct curl_slist *job_headers; +}; + + +/** + * Parse AML decision summary array. + * + * @param decisions JSON array with AML decision summaries + * @param[out] decision_ar where to write the result + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_aml_decisions (const json_t *decisions, + struct TALER_EXCHANGE_AmlDecisionSummary *decision_ar) +{ + json_t *obj; + size_t idx; + + json_array_foreach (decisions, idx, obj) + { + struct TALER_EXCHANGE_AmlDecisionSummary *decision = &decision_ar[idx]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_payto", + &decision->h_payto), + TALER_JSON_spec_aml_decision ("current_state", + &decision->current_state), + TALER_JSON_spec_amount_any ("threshold", + &decision->threshold), + GNUNET_JSON_spec_uint64 ("rowid", + &decision->rowid), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (obj, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Parse the provided decision data from the "200 OK" response. + * + * @param[in,out] lh handle (callback may be zero'ed out) + * @param json json reply with the data for one coin + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static enum GNUNET_GenericReturnValue +parse_decisions_ok (struct TALER_EXCHANGE_LookupAmlDecisions *lh, + const json_t *json) +{ + struct TALER_EXCHANGE_AmlDecisionsResponse lr = { + .hr.reply = json, + .hr.http_status = MHD_HTTP_OK + }; + const json_t *records; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("records", + &records), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + lr.details.ok.decisions_length = json_array_size (records); + { + struct TALER_EXCHANGE_AmlDecisionSummary decisions[ + GNUNET_NZL (lr.details.ok.decisions_length)]; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + + lr.details.ok.decisions = decisions; + ret = parse_aml_decisions (records, + decisions); + if (GNUNET_OK == ret) + { + lh->decisions_cb (lh->decisions_cb_cls, + &lr); + lh->decisions_cb = NULL; + } + return ret; + } +} + + +/** + * Function called when we're done processing the + * HTTP /aml/$OFFICER_PUB/decisions request. + * + * @param cls the `struct TALER_EXCHANGE_LookupAmlDecisions` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_lookup_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_LookupAmlDecisions *lh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_AmlDecisionsResponse lr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + lh->job = NULL; + switch (response_code) + { + case 0: + lr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + parse_decisions_ok (lh, + j)) + { + GNUNET_break_op (0); + lr.hr.http_status = 0; + lr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == lh->decisions_cb); + TALER_EXCHANGE_lookup_aml_decisions_cancel (lh); + return; + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_BAD_REQUEST: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, exchange says this coin was not melted; we + should pass the JSON reply to the application */ + break; + case MHD_HTTP_NOT_FOUND: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, exchange says this coin was not melted; we + should pass the JSON reply to the application */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + lr.hr.ec = TALER_JSON_get_error_code (j); + lr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for lookup AML decisions\n", + (unsigned int) response_code, + (int) lr.hr.ec); + break; + } + if (NULL != lh->decisions_cb) + lh->decisions_cb (lh->decisions_cb_cls, + &lr); + TALER_EXCHANGE_lookup_aml_decisions_cancel (lh); +} + + +struct TALER_EXCHANGE_LookupAmlDecisions * +TALER_EXCHANGE_lookup_aml_decisions ( + struct GNUNET_CURL_Context *ctx, + const char *exchange_url, + uint64_t start, + int delta, + enum TALER_AmlDecisionState state, + const struct TALER_AmlOfficerPrivateKeyP *officer_priv, + TALER_EXCHANGE_LookupAmlDecisionsCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_LookupAmlDecisions *lh; + CURL *eh; + struct TALER_AmlOfficerPublicKeyP officer_pub; + struct TALER_AmlOfficerSignatureP officer_sig; + char arg_str[sizeof (struct TALER_AmlOfficerPublicKeyP) * 2 + 32]; + const char *state_str = NULL; + + switch (state) + { + case TALER_AML_NORMAL: + state_str = "normal"; + break; + case TALER_AML_PENDING: + state_str = "pending"; + break; + case TALER_AML_FROZEN: + state_str = "frozen"; + break; + } + GNUNET_assert (NULL != state_str); + GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv, + &officer_pub.eddsa_pub); + TALER_officer_aml_query_sign (officer_priv, + &officer_sig); + { + char pub_str[sizeof (officer_pub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &officer_pub, + sizeof (officer_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "aml/%s/decisions/%s", + pub_str, + state_str); + } + lh = GNUNET_new (struct TALER_EXCHANGE_LookupAmlDecisions); + lh->decisions_cb = cb; + lh->decisions_cb_cls = cb_cls; + { + char delta_s[24]; + char start_s[24]; + + GNUNET_snprintf (delta_s, + sizeof (delta_s), + "%d", + delta); + GNUNET_snprintf (start_s, + sizeof (start_s), + "%llu", + (unsigned long long) start); + lh->url = TALER_url_join (exchange_url, + arg_str, + "delta", + delta_s, + "start", + start_s, + NULL); + } + if (NULL == lh->url) + { + GNUNET_free (lh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (lh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (lh->url); + GNUNET_free (lh); + return NULL; + } + { + char *hdr; + char sig_str[sizeof (officer_sig) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &officer_sig, + sizeof (officer_sig), + sig_str, + sizeof (sig_str)); + *end = '\0'; + + GNUNET_asprintf (&hdr, + "%s: %s", + TALER_AML_OFFICER_SIGNATURE_HEADER, + sig_str); + lh->job_headers = curl_slist_append (NULL, + hdr); + GNUNET_free (hdr); + lh->job_headers = curl_slist_append (lh->job_headers, + "Content-type: application/json"); + lh->job = GNUNET_CURL_job_add2 (ctx, + eh, + lh->job_headers, + &handle_lookup_finished, + lh); + } + return lh; +} + + +void +TALER_EXCHANGE_lookup_aml_decisions_cancel ( + struct TALER_EXCHANGE_LookupAmlDecisions *lh) +{ + if (NULL != lh->job) + { + GNUNET_CURL_job_cancel (lh->job); + lh->job = NULL; + } + curl_slist_free_all (lh->job_headers); + GNUNET_free (lh->url); + GNUNET_free (lh); +} + + +/* end of exchange_api_lookup_aml_decisions.c */ diff --git a/src/lib/exchange_api_management_add_partner.c b/src/lib/exchange_api_management_add_partner.c new file mode 100644 index 000000000..fec66c567 --- /dev/null +++ b/src/lib/exchange_api_management_add_partner.c @@ -0,0 +1,218 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_management_add_partner.c + * @brief functions to add an partner by an AML officer + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" +#include "taler_signatures.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" + + +struct TALER_EXCHANGE_ManagementAddPartner +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ManagementAddPartnerCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP POST /management/partners request. + * + * @param cls the `struct TALER_EXCHANGE_ManagementAddPartner *` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_add_partner_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ManagementAddPartner *wh = cls; + const json_t *json = response; + struct TALER_EXCHANGE_ManagementAddPartnerResponse apr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + wh->job = NULL; + switch (response_code) + { + case 0: + /* no reply */ + apr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + apr.hr.hint = "server offline?"; + break; + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_FORBIDDEN: + apr.hr.ec = TALER_JSON_get_error_code (json); + apr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_CONFLICT: + apr.hr.ec = TALER_JSON_get_error_code (json); + apr.hr.hint = TALER_JSON_get_error_hint (json); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + apr.hr.ec = TALER_JSON_get_error_code (json); + apr.hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for adding exchange partner\n", + (unsigned int) response_code, + (int) apr.hr.ec); + break; + } + if (NULL != wh->cb) + { + wh->cb (wh->cb_cls, + &apr); + wh->cb = NULL; + } + TALER_EXCHANGE_management_add_partner_cancel (wh); +} + + +struct TALER_EXCHANGE_ManagementAddPartner * +TALER_EXCHANGE_management_add_partner ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_MasterPublicKeyP *partner_pub, + struct GNUNET_TIME_Timestamp start_date, + struct GNUNET_TIME_Timestamp end_date, + struct GNUNET_TIME_Relative wad_frequency, + const struct TALER_Amount *wad_fee, + const char *partner_base_url, + const struct TALER_MasterSignatureP *master_sig, + TALER_EXCHANGE_ManagementAddPartnerCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ManagementAddPartner *wh; + CURL *eh; + json_t *body; + + wh = GNUNET_new (struct TALER_EXCHANGE_ManagementAddPartner); + wh->cb = cb; + wh->cb_cls = cb_cls; + wh->ctx = ctx; + wh->url = TALER_url_join (url, + "management/partners", + NULL); + if (NULL == wh->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (wh); + return NULL; + } + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("partner_base_url", + partner_base_url), + GNUNET_JSON_pack_timestamp ("start_date", + start_date), + GNUNET_JSON_pack_timestamp ("end_date", + end_date), + GNUNET_JSON_pack_time_rel ("wad_frequency", + wad_frequency), + GNUNET_JSON_pack_data_auto ("partner_pub", + &partner_pub), + GNUNET_JSON_pack_data_auto ("master_sig", + &master_sig), + TALER_JSON_pack_amount ("wad_fee", + wad_fee) + ); + eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&wh->post_ctx, + eh, + body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (wh->url); + return NULL; + } + json_decref (body); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + wh->url); + wh->job = GNUNET_CURL_job_add2 (ctx, + eh, + wh->post_ctx.headers, + &handle_add_partner_finished, + wh); + if (NULL == wh->job) + { + TALER_EXCHANGE_management_add_partner_cancel (wh); + return NULL; + } + return wh; +} + + +void +TALER_EXCHANGE_management_add_partner_cancel ( + struct TALER_EXCHANGE_ManagementAddPartner *wh) +{ + if (NULL != wh->job) + { + GNUNET_CURL_job_cancel (wh->job); + wh->job = NULL; + } + TALER_curl_easy_post_finished (&wh->post_ctx); + GNUNET_free (wh->url); + GNUNET_free (wh); +} diff --git a/src/lib/exchange_api_management_auditor_disable.c b/src/lib/exchange_api_management_auditor_disable.c index fb68ad6d3..8bce7f74f 100644 --- a/src/lib/exchange_api_management_auditor_disable.c +++ b/src/lib/exchange_api_management_auditor_disable.c @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -80,9 +81,9 @@ handle_auditor_disable_finished (void *cls, { struct TALER_EXCHANGE_ManagementAuditorDisableHandle *ah = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementAuditorDisableResponse adr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; ah->job = NULL; @@ -91,32 +92,32 @@ handle_auditor_disable_finished (void *cls, case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_CONFLICT: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + adr.hr.ec = TALER_JSON_get_error_code (json); + adr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management auditor disable\n", (unsigned int) response_code, - (int) hr.ec); + (int) adr.hr.ec); break; } if (NULL != ah->cb) { ah->cb (ah->cb_cls, - &hr); + &adr); ah->cb = NULL; } TALER_EXCHANGE_management_disable_auditor_cancel (ah); @@ -128,7 +129,7 @@ TALER_EXCHANGE_management_disable_auditor ( struct GNUNET_CURL_Context *ctx, const char *url, const struct TALER_AuditorPublicKeyP *auditor_pub, - struct GNUNET_TIME_Absolute validity_end, + struct GNUNET_TIME_Timestamp validity_end, const struct TALER_MasterSignatureP *master_sig, TALER_EXCHANGE_ManagementAuditorDisableCallback cb, void *cb_cls) @@ -169,28 +170,26 @@ TALER_EXCHANGE_management_disable_auditor ( body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("master_sig", master_sig), - GNUNET_JSON_pack_time_abs ("validity_end", - validity_end)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&ah->post_ctx, - eh, - body)) + GNUNET_JSON_pack_timestamp ("validity_end", + validity_end)); + eh = TALER_EXCHANGE_curl_easy_get_ (ah->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&ah->post_ctx, + eh, + body))) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (ah->url); - GNUNET_free (eh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", ah->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - ah->url)); ah->job = GNUNET_CURL_job_add2 (ctx, eh, ah->post_ctx.headers, diff --git a/src/lib/exchange_api_management_auditor_enable.c b/src/lib/exchange_api_management_auditor_enable.c index 1f53cb120..41c5049c2 100644 --- a/src/lib/exchange_api_management_auditor_enable.c +++ b/src/lib/exchange_api_management_auditor_enable.c @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -81,9 +82,9 @@ handle_auditor_enable_finished (void *cls, { struct TALER_EXCHANGE_ManagementAuditorEnableHandle *ah = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementAuditorEnableResponse aer = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; ah->job = NULL; @@ -92,28 +93,43 @@ handle_auditor_enable_finished (void *cls, case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + aer.hr.ec = TALER_JSON_get_error_code (json); + aer.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_NOT_FOUND: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + ah->url); + if (NULL != json) + { + aer.hr.ec = TALER_JSON_get_error_code (json); + aer.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + aer.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + aer.hr.hint = TALER_ErrorCode_get_hint (aer.hr.ec); + } break; case MHD_HTTP_CONFLICT: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + aer.hr.ec = TALER_JSON_get_error_code (json); + aer.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + aer.hr.ec = TALER_JSON_get_error_code (json); + aer.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management auditor enable\n", (unsigned int) response_code, - (int) hr.ec); + (int) aer.hr.ec); break; } if (NULL != ah->cb) { ah->cb (ah->cb_cls, - &hr); + &aer); ah->cb = NULL; } TALER_EXCHANGE_management_enable_auditor_cancel (ah); @@ -127,7 +143,7 @@ TALER_EXCHANGE_management_enable_auditor ( const struct TALER_AuditorPublicKeyP *auditor_pub, const char *auditor_url, const char *auditor_name, - struct GNUNET_TIME_Absolute validity_start, + struct GNUNET_TIME_Timestamp validity_start, const struct TALER_MasterSignatureP *master_sig, TALER_EXCHANGE_ManagementAuditorEnableCallback cb, void *cb_cls) @@ -159,28 +175,26 @@ TALER_EXCHANGE_management_enable_auditor ( auditor_pub), GNUNET_JSON_pack_data_auto ("master_sig", master_sig), - GNUNET_JSON_pack_time_abs ("validity_start", - validity_start)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&ah->post_ctx, - eh, - body)) + GNUNET_JSON_pack_timestamp ("validity_start", + validity_start)); + eh = TALER_EXCHANGE_curl_easy_get_ (ah->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&ah->post_ctx, + eh, + body)) ) { GNUNET_break (0); json_decref (body); + if (NULL != eh) + curl_easy_cleanup (eh); GNUNET_free (ah->url); - GNUNET_free (eh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", ah->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - ah->url)); ah->job = GNUNET_CURL_job_add2 (ctx, eh, ah->post_ctx.headers, diff --git a/src/lib/exchange_api_management_drain_profits.c b/src/lib/exchange_api_management_drain_profits.c new file mode 100644 index 000000000..bc7232b87 --- /dev/null +++ b/src/lib/exchange_api_management_drain_profits.c @@ -0,0 +1,213 @@ +/* + This file is part of TALER + Copyright (C) 2020-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_management_drain_profits.c + * @brief functions to set wire fees at an exchange + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "exchange_api_curl_defaults.h" +#include "taler_exchange_service.h" +#include "taler_signatures.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" + + +struct TALER_EXCHANGE_ManagementDrainProfitsHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ManagementDrainProfitsCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP /management/drain request. + * + * @param cls the `struct TALER_EXCHANGE_ManagementDrainProfitsHandle *` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_drain_profits_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp = cls; + const json_t *json = response; + struct TALER_EXCHANGE_ManagementDrainResponse dr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + dp->job = NULL; + switch (response_code) + { + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (json); + dr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_CONFLICT: + dr.hr.ec = TALER_JSON_get_error_code (json); + dr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_PRECONDITION_FAILED: + dr.hr.ec = TALER_JSON_get_error_code (json); + dr.hr.hint = TALER_JSON_get_error_hint (json); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + dr.hr.ec = TALER_JSON_get_error_code (json); + dr.hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange management drain profits\n", + (unsigned int) response_code, + (int) dr.hr.ec); + break; + } + if (NULL != dp->cb) + { + dp->cb (dp->cb_cls, + &dr); + dp->cb = NULL; + } + TALER_EXCHANGE_management_drain_profits_cancel (dp); +} + + +struct TALER_EXCHANGE_ManagementDrainProfitsHandle * +TALER_EXCHANGE_management_drain_profits ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Timestamp date, + const char *account_section, + const char *payto_uri, + const struct TALER_MasterSignatureP *master_sig, + TALER_EXCHANGE_ManagementDrainProfitsCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp; + CURL *eh; + json_t *body; + + dp = GNUNET_new (struct TALER_EXCHANGE_ManagementDrainProfitsHandle); + dp->cb = cb; + dp->cb_cls = cb_cls; + dp->ctx = ctx; + dp->url = TALER_url_join (url, + "management/drain", + NULL); + if (NULL == dp->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (dp); + return NULL; + } + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("debit_account_section", + account_section), + GNUNET_JSON_pack_string ("credit_payto_uri", + payto_uri), + GNUNET_JSON_pack_data_auto ("wtid", + wtid), + GNUNET_JSON_pack_data_auto ("master_sig", + master_sig), + GNUNET_JSON_pack_timestamp ("date", + date), + TALER_JSON_pack_amount ("amount", + amount)); + eh = TALER_EXCHANGE_curl_easy_get_ (dp->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&dp->post_ctx, + eh, + body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (dp->url); + return NULL; + } + json_decref (body); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + dp->url); + dp->job = GNUNET_CURL_job_add2 (ctx, + eh, + dp->post_ctx.headers, + &handle_drain_profits_finished, + dp); + if (NULL == dp->job) + { + TALER_EXCHANGE_management_drain_profits_cancel (dp); + return NULL; + } + return dp; +} + + +void +TALER_EXCHANGE_management_drain_profits_cancel ( + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp) +{ + if (NULL != dp->job) + { + GNUNET_CURL_job_cancel (dp->job); + dp->job = NULL; + } + TALER_curl_easy_post_finished (&dp->post_ctx); + GNUNET_free (dp->url); + GNUNET_free (dp); +} diff --git a/src/lib/exchange_api_management_get_keys.c b/src/lib/exchange_api_management_get_keys.c index e9cab5810..b88ddc205 100644 --- a/src/lib/exchange_api_management_get_keys.c +++ b/src/lib/exchange_api_management_get_keys.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2020 Taler Systems SA + Copyright (C) 2015-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 @@ -23,9 +23,10 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" -#include "taler_crypto_lib.h" +#include "taler_util.h" #include "taler_json_lib.h" /** @@ -74,25 +75,32 @@ struct TALER_EXCHANGE_ManagementGetKeysHandle * @param response the response * @return #GNUNET_OK if the response was well-formed */ -static int +static enum GNUNET_GenericReturnValue handle_ok (struct TALER_EXCHANGE_ManagementGetKeysHandle *gh, const json_t *response) { - struct TALER_EXCHANGE_FutureKeys fk; - json_t *sk; - json_t *dk; + struct TALER_EXCHANGE_ManagementGetKeysResponse gkr = { + .hr.http_status = MHD_HTTP_OK, + .hr.reply = response, + }; + struct TALER_EXCHANGE_FutureKeys *fk + = &gkr.details.ok.keys; + const json_t *sk; + const json_t *dk; bool ok; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("future_denoms", - &dk), - GNUNET_JSON_spec_json ("future_signkeys", - &sk), + GNUNET_JSON_spec_array_const ("future_denoms", + &dk), + GNUNET_JSON_spec_array_const ("future_signkeys", + &sk), GNUNET_JSON_spec_fixed_auto ("master_pub", - &fk.master_pub), + &fk->master_pub), GNUNET_JSON_spec_fixed_auto ("denom_secmod_public_key", - &fk.denom_secmod_public_key), + &fk->denom_secmod_public_key), + GNUNET_JSON_spec_fixed_auto ("denom_secmod_cs_public_key", + &fk->denom_secmod_cs_public_key), GNUNET_JSON_spec_fixed_auto ("signkey_secmod_public_key", - &fk.signkey_secmod_public_key), + &fk->signkey_secmod_public_key), GNUNET_JSON_spec_end () }; @@ -104,38 +112,38 @@ handle_ok (struct TALER_EXCHANGE_ManagementGetKeysHandle *gh, GNUNET_break_op (0); return GNUNET_SYSERR; } - fk.num_sign_keys = json_array_size (sk); - fk.num_denom_keys = json_array_size (dk); - fk.sign_keys = GNUNET_new_array ( - fk.num_sign_keys, + fk->num_sign_keys = json_array_size (sk); + fk->num_denom_keys = json_array_size (dk); + fk->sign_keys = GNUNET_new_array ( + fk->num_sign_keys, struct TALER_EXCHANGE_FutureSigningPublicKey); - fk.denom_keys = GNUNET_new_array ( - fk.num_denom_keys, + fk->denom_keys = GNUNET_new_array ( + fk->num_denom_keys, struct TALER_EXCHANGE_FutureDenomPublicKey); ok = true; - for (unsigned int i = 0; i<fk.num_sign_keys; i++) + for (unsigned int i = 0; i<fk->num_sign_keys; i++) { json_t *j = json_array_get (sk, i); struct TALER_EXCHANGE_FutureSigningPublicKey *sign_key - = &fk.sign_keys[i]; - struct GNUNET_JSON_Specification spec[] = { + = &fk->sign_keys[i]; + struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_fixed_auto ("key", &sign_key->key), GNUNET_JSON_spec_fixed_auto ("signkey_secmod_sig", &sign_key->signkey_secmod_sig), - TALER_JSON_spec_absolute_time ("stamp_start", - &sign_key->valid_from), - TALER_JSON_spec_absolute_time ("stamp_expire", - &sign_key->valid_until), - TALER_JSON_spec_absolute_time ("stamp_end", - &sign_key->valid_legal), + GNUNET_JSON_spec_timestamp ("stamp_start", + &sign_key->valid_from), + GNUNET_JSON_spec_timestamp ("stamp_expire", + &sign_key->valid_until), + GNUNET_JSON_spec_timestamp ("stamp_end", + &sign_key->valid_legal), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (j, - spec, + ispec, NULL, NULL)) { GNUNET_break_op (0); @@ -144,15 +152,15 @@ handle_ok (struct TALER_EXCHANGE_ManagementGetKeysHandle *gh, } { struct GNUNET_TIME_Relative duration - = GNUNET_TIME_absolute_get_difference (sign_key->valid_from, - sign_key->valid_until); + = GNUNET_TIME_absolute_get_difference (sign_key->valid_from.abs_time, + sign_key->valid_until.abs_time); if (GNUNET_OK != TALER_exchange_secmod_eddsa_verify ( &sign_key->key, sign_key->valid_from, duration, - &fk.signkey_secmod_public_key, + &fk->signkey_secmod_public_key, &sign_key->signkey_secmod_sig)) { GNUNET_break_op (0); @@ -161,26 +169,26 @@ handle_ok (struct TALER_EXCHANGE_ManagementGetKeysHandle *gh, } } } - for (unsigned int i = 0; i<fk.num_denom_keys; i++) + for (unsigned int i = 0; i<fk->num_denom_keys; i++) { json_t *j = json_array_get (dk, i); struct TALER_EXCHANGE_FutureDenomPublicKey *denom_key - = &fk.denom_keys[i]; + = &fk->denom_keys[i]; const char *section_name; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount_any ("value", &denom_key->value), - TALER_JSON_spec_absolute_time ("stamp_start", - &denom_key->valid_from), - TALER_JSON_spec_absolute_time ("stamp_expire_withdraw", - &denom_key->withdraw_valid_until), - TALER_JSON_spec_absolute_time ("stamp_expire_deposit", - &denom_key->expire_deposit), - TALER_JSON_spec_absolute_time ("stamp_expire_legal", - &denom_key->expire_legal), - GNUNET_JSON_spec_rsa_public_key ("denom_pub", - &denom_key->key.rsa_public_key), + GNUNET_JSON_spec_timestamp ("stamp_start", + &denom_key->valid_from), + GNUNET_JSON_spec_timestamp ("stamp_expire_withdraw", + &denom_key->withdraw_valid_until), + GNUNET_JSON_spec_timestamp ("stamp_expire_deposit", + &denom_key->expire_deposit), + GNUNET_JSON_spec_timestamp ("stamp_expire_legal", + &denom_key->expire_legal), + TALER_JSON_spec_denom_pub ("denom_pub", + &denom_key->key), TALER_JSON_spec_amount_any ("fee_withdraw", &denom_key->fee_withdraw), TALER_JSON_spec_amount_any ("fee_deposit", @@ -212,51 +220,76 @@ handle_ok (struct TALER_EXCHANGE_ManagementGetKeysHandle *gh, } { + struct TALER_DenominationHashP h_denom_pub; struct GNUNET_TIME_Relative duration - = GNUNET_TIME_absolute_get_difference (denom_key->valid_from, - denom_key->withdraw_valid_until); - struct GNUNET_HashCode h_denom_pub; + = GNUNET_TIME_absolute_get_difference ( + denom_key->valid_from.abs_time, + denom_key->withdraw_valid_until.abs_time); - GNUNET_CRYPTO_rsa_public_key_hash (denom_key->key.rsa_public_key, - &h_denom_pub); - if (GNUNET_OK != - TALER_exchange_secmod_rsa_verify (&h_denom_pub, - section_name, - denom_key->valid_from, - duration, - &fk.denom_secmod_public_key, - &denom_key->denom_secmod_sig)) + TALER_denom_pub_hash (&denom_key->key, + &h_denom_pub); + switch (denom_key->key.bsign_pub_key->cipher) { + case GNUNET_CRYPTO_BSA_RSA: + { + struct TALER_RsaPubHashP h_rsa; + + TALER_rsa_pub_hash ( + denom_key->key.bsign_pub_key->details.rsa_public_key, + &h_rsa); + if (GNUNET_OK != + TALER_exchange_secmod_rsa_verify (&h_rsa, + section_name, + denom_key->valid_from, + duration, + &fk->denom_secmod_public_key, + &denom_key->denom_secmod_sig)) + { + GNUNET_break_op (0); + ok = false; + break; + } + } + break; + case GNUNET_CRYPTO_BSA_CS: + { + struct TALER_CsPubHashP h_cs; + + TALER_cs_pub_hash ( + &denom_key->key.bsign_pub_key->details.cs_public_key, + &h_cs); + if (GNUNET_OK != + TALER_exchange_secmod_cs_verify (&h_cs, + section_name, + denom_key->valid_from, + duration, + &fk->denom_secmod_cs_public_key, + &denom_key->denom_secmod_sig)) + { + GNUNET_break_op (0); + ok = false; + break; + } + } + break; + default: GNUNET_break_op (0); ok = false; break; } } - GNUNET_JSON_parse_free (spec); + if (! ok) + break; } if (ok) { - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = MHD_HTTP_OK, - .reply = response - }; - gh->cb (gh->cb_cls, - &hr, - &fk); - } - for (unsigned int i = 0; i<fk.num_denom_keys; i++) - { - if (NULL != fk.denom_keys[i].key.rsa_public_key) - { - GNUNET_CRYPTO_rsa_public_key_free ( - fk.denom_keys[i].key.rsa_public_key); - fk.denom_keys[i].key.rsa_public_key = NULL; - } + &gkr); } - GNUNET_free (fk.sign_keys); - GNUNET_free (fk.denom_keys); - GNUNET_JSON_parse_free (spec); + for (unsigned int i = 0; i<fk->num_denom_keys; i++) + TALER_denom_pub_free (&fk->denom_keys[i].key); + GNUNET_free (fk->sign_keys); + GNUNET_free (fk->denom_keys); return (ok) ? GNUNET_OK : GNUNET_SYSERR; } @@ -276,9 +309,9 @@ handle_get_keys_finished (void *cls, { struct TALER_EXCHANGE_ManagementGetKeysHandle *gh = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementGetKeysResponse gkr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; gh->job = NULL; @@ -296,45 +329,49 @@ handle_get_keys_finished (void *cls, response_code = 0; } break; + case MHD_HTTP_NOT_FOUND: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + gh->url); + if (NULL != json) + { + gkr.hr.ec = TALER_JSON_get_error_code (json); + gkr.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + gkr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + gkr.hr.hint = TALER_ErrorCode_get_hint (gkr.hr.ec); + } + break; default: /* unexpected response code */ if (NULL != json) { - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + gkr.hr.ec = TALER_JSON_get_error_code (json); + gkr.hr.hint = TALER_JSON_get_error_hint (json); } else { - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = TALER_ErrorCode_get_hint (hr.ec); + gkr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + gkr.hr.hint = TALER_ErrorCode_get_hint (gkr.hr.ec); } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management get keys\n", (unsigned int) response_code, - (int) hr.ec); + (int) gkr.hr.ec); break; } if (NULL != gh->cb) { gh->cb (gh->cb_cls, - &hr, - NULL); + &gkr); gh->cb = NULL; } TALER_EXCHANGE_get_management_keys_cancel (gh); }; -/** - * Request future keys from the exchange. The obtained information will be - * passed to the @a cb. - * - * @param ctx the context - * @param url HTTP base URL for the exchange - * @param cb function to call with the exchange's future keys result - * @param cb_cls closure for @a cb - * @return the request handle; NULL upon error - */ struct TALER_EXCHANGE_ManagementGetKeysHandle * TALER_EXCHANGE_get_management_keys (struct GNUNET_CURL_Context *ctx, const char *url, @@ -358,14 +395,10 @@ TALER_EXCHANGE_get_management_keys (struct GNUNET_CURL_Context *ctx, GNUNET_free (gh); return NULL; } - eh = curl_easy_init (); + eh = TALER_EXCHANGE_curl_easy_get_ (gh->url); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", gh->url); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_URL, - gh->url)); gh->job = GNUNET_CURL_job_add (ctx, eh, &handle_get_keys_finished, @@ -379,11 +412,6 @@ TALER_EXCHANGE_get_management_keys (struct GNUNET_CURL_Context *ctx, } -/** - * Cancel #TALER_EXCHANGE_get_management_keys() operation. - * - * @param gh handle of the operation to cancel - */ void TALER_EXCHANGE_get_management_keys_cancel ( struct TALER_EXCHANGE_ManagementGetKeysHandle *gh) diff --git a/src/lib/exchange_api_management_post_extensions.c b/src/lib/exchange_api_management_post_extensions.c new file mode 100644 index 000000000..00d1c5e3f --- /dev/null +++ b/src/lib/exchange_api_management_post_extensions.c @@ -0,0 +1,213 @@ +/* + This file is part of TALER + Copyright (C) 2015-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 + <http://www.gnu.org/licenses/> + */ +/** + * @file lib/exchange_api_management_post_extensions.c + * @brief functions to handle the settings for extensions (p2p and age restriction) + * @author Özgür Kesim + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "taler_extensions.h" +#include "exchange_api_curl_defaults.h" +#include "taler_exchange_service.h" +#include "taler_signatures.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" + + +/** + * @brief Handle for a POST /management/extensions request. + */ +struct TALER_EXCHANGE_ManagementPostExtensionsHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ManagementPostExtensionsCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP POST /management/extensions request. + * + * @param cls the `struct TALER_EXCHANGE_ManagementPostExtensionsHandle *` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_post_extensions_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ManagementPostExtensionsHandle *ph = cls; + const json_t *json = response; + struct TALER_EXCHANGE_ManagementPostExtensionsResponse per = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + ph->job = NULL; + switch (response_code) + { + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_FORBIDDEN: + per.hr.ec = TALER_JSON_get_error_code (json); + per.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_NOT_FOUND: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + ph->url); + if (NULL != json) + { + per.hr.ec = TALER_JSON_get_error_code (json); + per.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + per.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + per.hr.hint = TALER_ErrorCode_get_hint (per.hr.ec); + } + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + per.hr.ec = TALER_JSON_get_error_code (json); + per.hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange management post extensions\n", + (unsigned int) response_code, + (int) per.hr.ec); + break; + } + if (NULL != ph->cb) + { + ph->cb (ph->cb_cls, + &per); + ph->cb = NULL; + } + TALER_EXCHANGE_management_post_extensions_cancel (ph); +} + + +struct TALER_EXCHANGE_ManagementPostExtensionsHandle * +TALER_EXCHANGE_management_post_extensions ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_EXCHANGE_ManagementPostExtensionsData *ped, + TALER_EXCHANGE_ManagementPostExtensionsCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ManagementPostExtensionsHandle *ph; + CURL *eh = NULL; + json_t *body = NULL; + + ph = GNUNET_new (struct TALER_EXCHANGE_ManagementPostExtensionsHandle); + ph->cb = cb; + ph->cb_cls = cb_cls; + ph->ctx = ctx; + ph->url = TALER_url_join (url, + "management/extensions", + NULL); + if (NULL == ph->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (ph); + return NULL; + } + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_steal ("extensions", + (json_t *) ped->extensions), + GNUNET_JSON_pack_data_auto ("extensions_sig", + &ped->extensions_sig)); + + eh = TALER_EXCHANGE_curl_easy_get_ (ph->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&ph->post_ctx, + eh, + body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (ph->url); + return NULL; + } + json_decref (body); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Requesting URL '%s'\n", + ph->url); + ph->job = GNUNET_CURL_job_add2 (ctx, + eh, + ph->post_ctx.headers, + &handle_post_extensions_finished, + ph); + if (NULL == ph->job) + { + TALER_EXCHANGE_management_post_extensions_cancel (ph); + return NULL; + } + return ph; +} + + +void +TALER_EXCHANGE_management_post_extensions_cancel ( + struct TALER_EXCHANGE_ManagementPostExtensionsHandle *ph) +{ + if (NULL != ph->job) + { + GNUNET_CURL_job_cancel (ph->job); + ph->job = NULL; + } + TALER_curl_easy_post_finished (&ph->post_ctx); + GNUNET_free (ph->url); + GNUNET_free (ph); +} diff --git a/src/lib/exchange_api_management_post_keys.c b/src/lib/exchange_api_management_post_keys.c index e956cfd55..a46124d90 100644 --- a/src/lib/exchange_api_management_post_keys.c +++ b/src/lib/exchange_api_management_post_keys.c @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -81,9 +82,9 @@ handle_post_keys_finished (void *cls, { struct TALER_EXCHANGE_ManagementPostKeysHandle *ph = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementPostKeysResponse pkr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; ph->job = NULL; @@ -92,28 +93,32 @@ handle_post_keys_finished (void *cls, case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + pkr.hr.ec = TALER_JSON_get_error_code (json); + pkr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + pkr.hr.ec = TALER_JSON_get_error_code (json); + pkr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_REQUEST_ENTITY_TOO_LARGE: + pkr.hr.ec = TALER_JSON_get_error_code (json); + pkr.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + pkr.hr.ec = TALER_JSON_get_error_code (json); + pkr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management post keys\n", (unsigned int) response_code, - (int) hr.ec); + (int) pkr.hr.ec); break; } if (NULL != ph->cb) { ph->cb (ph->cb_cls, - &hr); + &pkr); ph->cb = NULL; } TALER_EXCHANGE_post_management_keys_cancel (ph); @@ -185,26 +190,24 @@ TALER_EXCHANGE_post_management_keys ( denom_sigs), GNUNET_JSON_pack_array_steal ("signkey_sigs", signkey_sigs)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&ph->post_ctx, - eh, - body)) + eh = TALER_EXCHANGE_curl_easy_get_ (ph->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&ph->post_ctx, + eh, + body)) ) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (ph->url); - GNUNET_free (eh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", ph->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - ph->url)); ph->job = GNUNET_CURL_job_add2 (ctx, eh, ph->post_ctx.headers, diff --git a/src/lib/exchange_api_management_revoke_denomination_key.c b/src/lib/exchange_api_management_revoke_denomination_key.c index b9e2a556a..a57704776 100644 --- a/src/lib/exchange_api_management_revoke_denomination_key.c +++ b/src/lib/exchange_api_management_revoke_denomination_key.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2020 Taler Systems SA + Copyright (C) 2015-2024 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 @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -81,9 +82,9 @@ handle_revoke_denomination_finished (void *cls, { struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle *rh = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementRevokeDenominationResponse rdr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; rh->job = NULL; @@ -91,30 +92,30 @@ handle_revoke_denomination_finished (void *cls, { case 0: /* no reply */ - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = "server offline?"; + rdr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + rdr.hr.hint = "server offline?"; break; case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + rdr.hr.ec = TALER_JSON_get_error_code (json); + rdr.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + rdr.hr.ec = TALER_JSON_get_error_code (json); + rdr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management revoke denomination\n", (unsigned int) response_code, - (int) hr.ec); + (int) rdr.hr.ec); break; } if (NULL != rh->cb) { rh->cb (rh->cb_cls, - &hr); + &rdr); rh->cb = NULL; } TALER_EXCHANGE_management_revoke_denomination_key_cancel (rh); @@ -125,7 +126,7 @@ struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle * TALER_EXCHANGE_management_revoke_denomination_key ( struct GNUNET_CURL_Context *ctx, const char *url, - const struct GNUNET_HashCode *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_MasterSignatureP *master_sig, TALER_EXCHANGE_ManagementRevokeDenominationKeyCallback cb, void *cb_cls) @@ -173,25 +174,25 @@ TALER_EXCHANGE_management_revoke_denomination_key ( GNUNET_free (rh); return NULL; } - eh = curl_easy_init (); - if (GNUNET_OK != - TALER_curl_easy_post (&rh->post_ctx, - eh, - body)) + eh = TALER_EXCHANGE_curl_easy_get_ (rh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&rh->post_ctx, + eh, + body)) ) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (rh->url); - GNUNET_free (eh); + GNUNET_free (rh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", rh->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - rh->url)); rh->job = GNUNET_CURL_job_add2 (ctx, eh, rh->post_ctx.headers, diff --git a/src/lib/exchange_api_management_revoke_signing_key.c b/src/lib/exchange_api_management_revoke_signing_key.c index f43a46c40..d2fa78264 100644 --- a/src/lib/exchange_api_management_revoke_signing_key.c +++ b/src/lib/exchange_api_management_revoke_signing_key.c @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -78,9 +79,9 @@ handle_revoke_signing_finished (void *cls, { struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle *rh = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementRevokeSigningKeyResponse rsr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; rh->job = NULL; @@ -88,30 +89,30 @@ handle_revoke_signing_finished (void *cls, { case 0: /* no reply */ - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = "server offline?"; + rsr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + rsr.hr.hint = "server offline?"; break; case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + rsr.hr.ec = TALER_JSON_get_error_code (json); + rsr.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + rsr.hr.ec = TALER_JSON_get_error_code (json); + rsr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management revoke signkey\n", (unsigned int) response_code, - (int) hr.ec); + (int) rsr.hr.ec); break; } if (NULL != rh->cb) { rh->cb (rh->cb_cls, - &hr); + &rsr); rh->cb = NULL; } TALER_EXCHANGE_management_revoke_signing_key_cancel (rh); @@ -163,26 +164,25 @@ TALER_EXCHANGE_management_revoke_signing_key ( body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("master_sig", master_sig)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&rh->post_ctx, - eh, - body)) + eh = TALER_EXCHANGE_curl_easy_get_ (rh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&rh->post_ctx, + eh, + body)) ) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (rh->url); - GNUNET_free (eh); + GNUNET_free (rh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", rh->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - rh->url)); rh->job = GNUNET_CURL_job_add2 (ctx, eh, rh->post_ctx.headers, diff --git a/src/lib/exchange_api_management_set_global_fee.c b/src/lib/exchange_api_management_set_global_fee.c new file mode 100644 index 000000000..f6282a812 --- /dev/null +++ b/src/lib/exchange_api_management_set_global_fee.c @@ -0,0 +1,236 @@ +/* + This file is part of TALER + Copyright (C) 2020-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_management_set_global_fee.c + * @brief functions to set global fees at an exchange + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "exchange_api_curl_defaults.h" +#include "taler_exchange_service.h" +#include "taler_signatures.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" + + +struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ManagementSetGlobalFeeCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP /management/global request. + * + * @param cls the `struct TALER_EXCHANGE_ManagementAuditorEnableHandle *` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_set_global_fee_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle *sgfh = cls; + const json_t *json = response; + struct TALER_EXCHANGE_ManagementSetGlobalFeeResponse sfr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + sgfh->job = NULL; + switch (response_code) + { + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_FORBIDDEN: + sfr.hr.ec = TALER_JSON_get_error_code (json); + sfr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_NOT_FOUND: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + sgfh->url); + if (NULL != json) + { + sfr.hr.ec = TALER_JSON_get_error_code (json); + sfr.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + sfr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + sfr.hr.hint = TALER_ErrorCode_get_hint (sfr.hr.ec); + } + break; + case MHD_HTTP_CONFLICT: + sfr.hr.ec = TALER_JSON_get_error_code (json); + sfr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_PRECONDITION_FAILED: + sfr.hr.ec = TALER_JSON_get_error_code (json); + sfr.hr.hint = TALER_JSON_get_error_hint (json); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + sfr.hr.ec = TALER_JSON_get_error_code (json); + sfr.hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange management set global fee\n", + (unsigned int) response_code, + (int) sfr.hr.ec); + break; + } + if (NULL != sgfh->cb) + { + sgfh->cb (sgfh->cb_cls, + &sfr); + sgfh->cb = NULL; + } + TALER_EXCHANGE_management_set_global_fees_cancel (sgfh); +} + + +struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle * +TALER_EXCHANGE_management_set_global_fees ( + struct GNUNET_CURL_Context *ctx, + const char *exchange_base_url, + struct GNUNET_TIME_Timestamp validity_start, + struct GNUNET_TIME_Timestamp validity_end, + const struct TALER_GlobalFeeSet *fees, + struct GNUNET_TIME_Relative purse_timeout, + struct GNUNET_TIME_Relative history_expiration, + uint32_t purse_account_limit, + const struct TALER_MasterSignatureP *master_sig, + TALER_EXCHANGE_ManagementSetGlobalFeeCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle *sgfh; + CURL *eh; + json_t *body; + + sgfh = GNUNET_new (struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle); + sgfh->cb = cb; + sgfh->cb_cls = cb_cls; + sgfh->ctx = ctx; + sgfh->url = TALER_url_join (exchange_base_url, + "management/global-fee", + NULL); + if (NULL == sgfh->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (sgfh); + return NULL; + } + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("master_sig", + master_sig), + GNUNET_JSON_pack_timestamp ("fee_start", + validity_start), + GNUNET_JSON_pack_timestamp ("fee_end", + validity_end), + TALER_JSON_pack_amount ("history_fee", + &fees->history), + TALER_JSON_pack_amount ("account_fee", + &fees->account), + TALER_JSON_pack_amount ("purse_fee", + &fees->purse), + GNUNET_JSON_pack_time_rel ("purse_timeout", + purse_timeout), + GNUNET_JSON_pack_time_rel ("history_expiration", + history_expiration), + GNUNET_JSON_pack_uint64 ("purse_account_limit", + purse_account_limit)); + eh = TALER_EXCHANGE_curl_easy_get_ (sgfh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&sgfh->post_ctx, + eh, + body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (sgfh->url); + GNUNET_free (sgfh); + return NULL; + } + json_decref (body); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + sgfh->url); + sgfh->job = GNUNET_CURL_job_add2 (ctx, + eh, + sgfh->post_ctx.headers, + &handle_set_global_fee_finished, + sgfh); + if (NULL == sgfh->job) + { + TALER_EXCHANGE_management_set_global_fees_cancel (sgfh); + return NULL; + } + return sgfh; +} + + +void +TALER_EXCHANGE_management_set_global_fees_cancel ( + struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle *sgfh) +{ + if (NULL != sgfh->job) + { + GNUNET_CURL_job_cancel (sgfh->job); + sgfh->job = NULL; + } + TALER_curl_easy_post_finished (&sgfh->post_ctx); + GNUNET_free (sgfh->url); + GNUNET_free (sgfh); +} diff --git a/src/lib/exchange_api_management_set_wire_fee.c b/src/lib/exchange_api_management_set_wire_fee.c index 075dbbbba..aaeae21f4 100644 --- a/src/lib/exchange_api_management_set_wire_fee.c +++ b/src/lib/exchange_api_management_set_wire_fee.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020-2021 Taler Systems SA + Copyright (C) 2020-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 @@ -22,6 +22,7 @@ #include "platform.h" #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> +#include "exchange_api_curl_defaults.h" #include "taler_exchange_service.h" #include "taler_signatures.h" #include "taler_curl_lib.h" @@ -78,9 +79,9 @@ handle_set_wire_fee_finished (void *cls, { struct TALER_EXCHANGE_ManagementSetWireFeeHandle *swfh = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementSetWireFeeResponse swr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; swfh->job = NULL; @@ -89,32 +90,47 @@ handle_set_wire_fee_finished (void *cls, case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + swr.hr.ec = TALER_JSON_get_error_code (json); + swr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_NOT_FOUND: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + swfh->url); + if (NULL != json) + { + swr.hr.ec = TALER_JSON_get_error_code (json); + swr.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + swr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + swr.hr.hint = TALER_ErrorCode_get_hint (swr.hr.ec); + } break; case MHD_HTTP_CONFLICT: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + swr.hr.ec = TALER_JSON_get_error_code (json); + swr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_PRECONDITION_FAILED: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + swr.hr.ec = TALER_JSON_get_error_code (json); + swr.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + swr.hr.ec = TALER_JSON_get_error_code (json); + swr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management set wire fee\n", (unsigned int) response_code, - (int) hr.ec); + (int) swr.hr.ec); break; } if (NULL != swfh->cb) { swfh->cb (swfh->cb_cls, - &hr); + &swr); swfh->cb = NULL; } TALER_EXCHANGE_management_set_wire_fees_cancel (swfh); @@ -126,12 +142,11 @@ TALER_EXCHANGE_management_set_wire_fees ( struct GNUNET_CURL_Context *ctx, const char *exchange_base_url, const char *wire_method, - struct GNUNET_TIME_Absolute validity_start, - struct GNUNET_TIME_Absolute validity_end, - const struct TALER_Amount *wire_fee, - const struct TALER_Amount *closing_fee, + struct GNUNET_TIME_Timestamp validity_start, + struct GNUNET_TIME_Timestamp validity_end, + const struct TALER_WireFeeSet *fees, const struct TALER_MasterSignatureP *master_sig, - TALER_EXCHANGE_ManagementWireEnableCallback cb, + TALER_EXCHANGE_ManagementSetWireFeeCallback cb, void *cb_cls) { struct TALER_EXCHANGE_ManagementSetWireFeeHandle *swfh; @@ -157,34 +172,33 @@ TALER_EXCHANGE_management_set_wire_fees ( wire_method), GNUNET_JSON_pack_data_auto ("master_sig", master_sig), - GNUNET_JSON_pack_time_abs ("fee_start", - validity_start), - GNUNET_JSON_pack_time_abs ("fee_end", - validity_end), + GNUNET_JSON_pack_timestamp ("fee_start", + validity_start), + GNUNET_JSON_pack_timestamp ("fee_end", + validity_end), TALER_JSON_pack_amount ("closing_fee", - closing_fee), + &fees->closing), TALER_JSON_pack_amount ("wire_fee", - wire_fee)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&swfh->post_ctx, - eh, - body)) + &fees->wire)); + eh = TALER_EXCHANGE_curl_easy_get_ (swfh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&swfh->post_ctx, + eh, + body)) ) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (swfh->url); - GNUNET_free (eh); + GNUNET_free (swfh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", swfh->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - swfh->url)); swfh->job = GNUNET_CURL_job_add2 (ctx, eh, swfh->post_ctx.headers, diff --git a/src/lib/exchange_api_management_update_aml_officer.c b/src/lib/exchange_api_management_update_aml_officer.c new file mode 100644 index 000000000..af0169b02 --- /dev/null +++ b/src/lib/exchange_api_management_update_aml_officer.c @@ -0,0 +1,230 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_management_update_aml_officer.c + * @brief functions to update AML officer status + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" +#include "taler_signatures.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" + + +struct TALER_EXCHANGE_ManagementUpdateAmlOfficer +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ManagementUpdateAmlOfficerCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP /management/wire request. + * + * @param cls the `struct TALER_EXCHANGE_ManagementAuditorEnableHandle *` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_update_aml_officer_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ManagementUpdateAmlOfficer *wh = cls; + const json_t *json = response; + struct TALER_EXCHANGE_ManagementUpdateAmlOfficerResponse uar = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + wh->job = NULL; + switch (response_code) + { + case 0: + /* no reply */ + uar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + uar.hr.hint = "server offline?"; + break; + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_FORBIDDEN: + uar.hr.ec = TALER_JSON_get_error_code (json); + uar.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_NOT_FOUND: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + wh->url); + if (NULL != json) + { + uar.hr.ec = TALER_JSON_get_error_code (json); + uar.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + uar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + uar.hr.hint = TALER_ErrorCode_get_hint (uar.hr.ec); + } + break; + case MHD_HTTP_CONFLICT: + uar.hr.ec = TALER_JSON_get_error_code (json); + uar.hr.hint = TALER_JSON_get_error_hint (json); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + uar.hr.ec = TALER_JSON_get_error_code (json); + uar.hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange management update AML officer\n", + (unsigned int) response_code, + (int) uar.hr.ec); + break; + } + if (NULL != wh->cb) + { + wh->cb (wh->cb_cls, + &uar); + wh->cb = NULL; + } + TALER_EXCHANGE_management_update_aml_officer_cancel (wh); +} + + +struct TALER_EXCHANGE_ManagementUpdateAmlOfficer * +TALER_EXCHANGE_management_update_aml_officer ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *officer_name, + struct GNUNET_TIME_Timestamp change_date, + bool is_active, + bool read_only, + const struct TALER_MasterSignatureP *master_sig, + TALER_EXCHANGE_ManagementUpdateAmlOfficerCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ManagementUpdateAmlOfficer *wh; + CURL *eh; + json_t *body; + + wh = GNUNET_new (struct TALER_EXCHANGE_ManagementUpdateAmlOfficer); + wh->cb = cb; + wh->cb_cls = cb_cls; + wh->ctx = ctx; + wh->url = TALER_url_join (url, + "management/aml-officers", + NULL); + if (NULL == wh->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (wh); + return NULL; + } + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("officer_name", + officer_name), + GNUNET_JSON_pack_data_auto ("officer_pub", + officer_pub), + GNUNET_JSON_pack_data_auto ("master_sig", + master_sig), + GNUNET_JSON_pack_bool ("is_active", + is_active), + GNUNET_JSON_pack_bool ("read_only", + read_only), + GNUNET_JSON_pack_timestamp ("change_date", + change_date)); + eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&wh->post_ctx, + eh, + body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (wh->url); + GNUNET_free (wh); + return NULL; + } + json_decref (body); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + wh->url); + wh->job = GNUNET_CURL_job_add2 (ctx, + eh, + wh->post_ctx.headers, + &handle_update_aml_officer_finished, + wh); + if (NULL == wh->job) + { + TALER_EXCHANGE_management_update_aml_officer_cancel (wh); + return NULL; + } + return wh; +} + + +void +TALER_EXCHANGE_management_update_aml_officer_cancel ( + struct TALER_EXCHANGE_ManagementUpdateAmlOfficer *wh) +{ + if (NULL != wh->job) + { + GNUNET_CURL_job_cancel (wh->job); + wh->job = NULL; + } + TALER_curl_easy_post_finished (&wh->post_ctx); + GNUNET_free (wh->url); + GNUNET_free (wh); +} diff --git a/src/lib/exchange_api_management_wire_disable.c b/src/lib/exchange_api_management_wire_disable.c index fbc6626ad..23b10c58c 100644 --- a/src/lib/exchange_api_management_wire_disable.c +++ b/src/lib/exchange_api_management_wire_disable.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2021 Taler Systems SA + Copyright (C) 2015-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 @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -78,9 +79,9 @@ handle_auditor_disable_finished (void *cls, { struct TALER_EXCHANGE_ManagementWireDisableHandle *wh = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementWireDisableResponse wdr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; wh->job = NULL; @@ -88,38 +89,49 @@ handle_auditor_disable_finished (void *cls, { case 0: /* no reply */ - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = "server offline?"; + wdr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + wdr.hr.hint = "server offline?"; break; case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + wdr.hr.ec = TALER_JSON_get_error_code (json); + wdr.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_NOT_FOUND: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + wh->url); + if (NULL != json) + { + wdr.hr.ec = TALER_JSON_get_error_code (json); + wdr.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + wdr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + wdr.hr.hint = TALER_ErrorCode_get_hint (wdr.hr.ec); + } break; case MHD_HTTP_CONFLICT: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + wdr.hr.ec = TALER_JSON_get_error_code (json); + wdr.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + wdr.hr.ec = TALER_JSON_get_error_code (json); + wdr.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d exchange management disable wire\n", (unsigned int) response_code, - (int) hr.ec); + (int) wdr.hr.ec); break; } if (NULL != wh->cb) { wh->cb (wh->cb_cls, - &hr); + &wdr); wh->cb = NULL; } TALER_EXCHANGE_management_disable_wire_cancel (wh); @@ -131,7 +143,7 @@ TALER_EXCHANGE_management_disable_wire ( struct GNUNET_CURL_Context *ctx, const char *url, const char *payto_uri, - struct GNUNET_TIME_Absolute validity_end, + struct GNUNET_TIME_Timestamp validity_end, const struct TALER_MasterSignatureP *master_sig, TALER_EXCHANGE_ManagementWireDisableCallback cb, void *cb_cls) @@ -159,28 +171,27 @@ TALER_EXCHANGE_management_disable_wire ( payto_uri), GNUNET_JSON_pack_data_auto ("master_sig_del", master_sig), - GNUNET_JSON_pack_time_abs ("validity_end", - validity_end)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&wh->post_ctx, - eh, - body)) + GNUNET_JSON_pack_timestamp ("validity_end", + validity_end)); + eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&wh->post_ctx, + eh, + body)) ) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (wh->url); - GNUNET_free (eh); + GNUNET_free (wh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", wh->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - wh->url)); wh->job = GNUNET_CURL_job_add2 (ctx, eh, wh->post_ctx.headers, diff --git a/src/lib/exchange_api_management_wire_enable.c b/src/lib/exchange_api_management_wire_enable.c index 43eb2ce97..9a163b558 100644 --- a/src/lib/exchange_api_management_wire_enable.c +++ b/src/lib/exchange_api_management_wire_enable.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2021 Taler Systems SA + Copyright (C) 2015-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 @@ -23,6 +23,7 @@ #include "taler_json_lib.h" #include <gnunet/gnunet_curl_lib.h> #include "taler_exchange_service.h" +#include "exchange_api_curl_defaults.h" #include "taler_signatures.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" @@ -78,9 +79,9 @@ handle_auditor_enable_finished (void *cls, { struct TALER_EXCHANGE_ManagementWireEnableHandle *wh = cls; const json_t *json = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .http_status = (unsigned int) response_code, - .reply = json + struct TALER_EXCHANGE_ManagementWireEnableResponse wer = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json }; wh->job = NULL; @@ -88,34 +89,49 @@ handle_auditor_enable_finished (void *cls, { case 0: /* no reply */ - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - hr.hint = "server offline?"; + wer.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + wer.hr.hint = "server offline?"; break; case MHD_HTTP_NO_CONTENT: break; case MHD_HTTP_FORBIDDEN: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + wer.hr.ec = TALER_JSON_get_error_code (json); + wer.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_NOT_FOUND: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Server did not find handler at `%s'. Did you configure the correct exchange base URL?\n", + wh->url); + if (NULL != json) + { + wer.hr.ec = TALER_JSON_get_error_code (json); + wer.hr.hint = TALER_JSON_get_error_hint (json); + } + else + { + wer.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + wer.hr.hint = TALER_ErrorCode_get_hint (wer.hr.ec); + } break; case MHD_HTTP_CONFLICT: - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + wer.hr.ec = TALER_JSON_get_error_code (json); + wer.hr.hint = TALER_JSON_get_error_hint (json); break; default: /* unexpected response code */ GNUNET_break_op (0); - hr.ec = TALER_JSON_get_error_code (json); - hr.hint = TALER_JSON_get_error_hint (json); + wer.hr.ec = TALER_JSON_get_error_code (json); + wer.hr.hint = TALER_JSON_get_error_hint (json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange management enable wire\n", (unsigned int) response_code, - (int) hr.ec); + (int) wer.hr.ec); break; } if (NULL != wh->cb) { wh->cb (wh->cb_cls, - &hr); + &wer); wh->cb = NULL; } TALER_EXCHANGE_management_enable_wire_cancel (wh); @@ -127,9 +143,14 @@ TALER_EXCHANGE_management_enable_wire ( struct GNUNET_CURL_Context *ctx, const char *url, const char *payto_uri, - struct GNUNET_TIME_Absolute validity_start, + const char *conversion_url, + const json_t *debit_restrictions, + const json_t *credit_restrictions, + struct GNUNET_TIME_Timestamp validity_start, const struct TALER_MasterSignatureP *master_sig1, const struct TALER_MasterSignatureP *master_sig2, + const char *bank_label, + int64_t priority, TALER_EXCHANGE_ManagementWireEnableCallback cb, void *cb_cls) { @@ -137,6 +158,18 @@ TALER_EXCHANGE_management_enable_wire ( CURL *eh; json_t *body; + { + char *msg = TALER_payto_validate (payto_uri); + + if (NULL != msg) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "payto URI is malformed: %s\n", + msg); + GNUNET_free (msg); + return NULL; + } + } wh = GNUNET_new (struct TALER_EXCHANGE_ManagementWireEnableHandle); wh->cb = cb; wh->cb_cls = cb_cls; @@ -154,32 +187,43 @@ TALER_EXCHANGE_management_enable_wire ( body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("payto_uri", payto_uri), + GNUNET_JSON_pack_array_incref ("debit_restrictions", + (json_t *) debit_restrictions), + GNUNET_JSON_pack_array_incref ("credit_restrictions", + (json_t *) credit_restrictions), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("conversion_url", + conversion_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("bank_label", + bank_label)), + GNUNET_JSON_pack_int64 ("priority", + priority), GNUNET_JSON_pack_data_auto ("master_sig_add", master_sig1), GNUNET_JSON_pack_data_auto ("master_sig_wire", master_sig2), - GNUNET_JSON_pack_time_abs ("validity_start", - validity_start)); - eh = curl_easy_init (); - GNUNET_assert (NULL != eh); - if (GNUNET_OK != - TALER_curl_easy_post (&wh->post_ctx, - eh, - body)) + GNUNET_JSON_pack_timestamp ("validity_start", + validity_start)); + eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&wh->post_ctx, + eh, + body)) ) { GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); json_decref (body); GNUNET_free (wh->url); - GNUNET_free (eh); + GNUNET_free (wh); return NULL; } json_decref (body); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", wh->url); - GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, - CURLOPT_URL, - wh->url)); wh->job = GNUNET_CURL_job_add2 (ctx, eh, wh->post_ctx.headers, diff --git a/src/lib/exchange_api_melt.c b/src/lib/exchange_api_melt.c index dcee66a8b..c2f8cefb7 100644 --- a/src/lib/exchange_api_melt.c +++ b/src/lib/exchange_api_melt.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2021 Taler Systems SA + Copyright (C) 2015-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 @@ -27,6 +27,7 @@ #include <gnunet/gnunet_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" @@ -40,9 +41,9 @@ struct TALER_EXCHANGE_MeltHandle { /** - * The connection to exchange this request handle will use + * The keys of the this request handle will use */ - struct TALER_EXCHANGE_Handle *exchange; + struct TALER_EXCHANGE_Keys *keys; /** * The url for this request. @@ -50,6 +51,16 @@ struct TALER_EXCHANGE_MeltHandle char *url; /** + * The exchange base url. + */ + char *exchange_url; + + /** + * Curl context. + */ + struct GNUNET_CURL_Context *cctx; + + /** * Context for #TEH_curl_easy_post(). Keeps the data that must * persist for Curl to make the upload. */ @@ -73,7 +84,28 @@ struct TALER_EXCHANGE_MeltHandle /** * Actual information about the melt operation. */ - struct MeltData *md; + struct MeltData md; + + /** + * The secret the entire melt operation is seeded from. + */ + struct TALER_RefreshMasterSecretP rms; + + /** + * Details about the characteristics of the requested melt operation. + */ + const struct TALER_EXCHANGE_RefreshData *rd; + + /** + * Array of `num_fresh_coins` per-coin values + * returned from melt operation. + */ + struct TALER_EXCHANGE_MeltBlindingDetail *mbds; + + /** + * Handle for the preflight request, or NULL. + */ + struct TALER_EXCHANGE_CsRMeltHandle *csr; /** * Public key of the coin being melted. @@ -81,9 +113,24 @@ struct TALER_EXCHANGE_MeltHandle struct TALER_CoinSpendPublicKeyP coin_pub; /** + * Signature affirming the melt. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** * @brief Public information about the coin's denomination key */ - struct TALER_EXCHANGE_DenomPublicKey dki; + const struct TALER_EXCHANGE_DenomPublicKey *dki; + + /** + * Gamma value chosen by the exchange during melt. + */ + uint32_t noreveal_index; + + /** + * True if we need to include @e rms in our melt request. + */ + bool send_rms; }; @@ -91,27 +138,27 @@ struct TALER_EXCHANGE_MeltHandle * Verify that the signature on the "200 OK" response * from the exchange is valid. * - * @param mh melt handle + * @param[in,out] mh melt handle * @param json json reply with the signature * @param[out] exchange_pub public key of the exchange used for the signature - * @param[out] noreveal_index set to the noreveal index selected by the exchange * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not */ -static int +static enum GNUNET_GenericReturnValue verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh, const json_t *json, - struct TALER_ExchangePublicKeyP *exchange_pub, - uint32_t *noreveal_index) + struct TALER_ExchangePublicKeyP *exchange_pub) { struct TALER_ExchangeSignatureP exchange_sig; const struct TALER_EXCHANGE_Keys *key_state; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("exchange_sig", &exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", exchange_pub), - GNUNET_JSON_spec_uint32 ("noreveal_index", noreveal_index), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + exchange_pub), + GNUNET_JSON_spec_uint32 ("noreveal_index", + &mh->noreveal_index), GNUNET_JSON_spec_end () }; - struct TALER_RefreshMeltConfirmationPS confirm; if (GNUNET_OK != GNUNET_JSON_parse (json, @@ -121,9 +168,8 @@ verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh, GNUNET_break_op (0); return GNUNET_SYSERR; } - /* check that exchange signing key is permitted */ - key_state = TALER_EXCHANGE_get_keys (mh->exchange); + key_state = mh->keys; if (GNUNET_OK != TALER_EXCHANGE_test_signing_key (key_state, exchange_pub)) @@ -133,23 +179,18 @@ verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh, } /* check that noreveal index is in permitted range */ - if (TALER_CNC_KAPPA <= *noreveal_index) + if (TALER_CNC_KAPPA <= mh->noreveal_index) { GNUNET_break_op (0); return GNUNET_SYSERR; } - /* verify signature by exchange */ - confirm.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_MELT); - confirm.purpose.size = htonl (sizeof (struct - TALER_RefreshMeltConfirmationPS)); - confirm.rc = mh->md->rc; - confirm.noreveal_index = htonl (*noreveal_index); if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_EXCHANGE_CONFIRM_MELT, - &confirm, - &exchange_sig.eddsa_signature, - &exchange_pub->eddsa_pub)) + TALER_exchange_online_melt_confirmation_verify ( + &mh->md.rc, + mh->noreveal_index, + exchange_pub, + &exchange_sig)) { GNUNET_break_op (0); return GNUNET_SYSERR; @@ -159,167 +200,6 @@ verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh, /** - * Verify that the signatures on the "409 CONFLICT" response from the - * exchange demonstrating customer denomination key differences - * resulting from coin private key reuse are valid. - * - * @param mh melt handle - * @param json json reply with the signature(s) and transaction history - * @return #GNUNET_OK if the signature(s) is valid, #GNUNET_SYSERR if not - */ -static int -verify_melt_signature_denom_conflict (struct TALER_EXCHANGE_MeltHandle *mh, - const json_t *json) - -{ - json_t *history; - struct TALER_Amount total; - struct GNUNET_HashCode h_denom_pub; - - memset (&h_denom_pub, - 0, - sizeof (h_denom_pub)); - history = json_object_get (json, - "history"); - if (GNUNET_OK != - TALER_EXCHANGE_verify_coin_history (&mh->dki, - mh->dki.value.currency, - &mh->coin_pub, - history, - &h_denom_pub, - &total)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 != GNUNET_memcmp (&mh->dki.h_key, - &h_denom_pub)) - return GNUNET_OK; /* indeed, proof with different denomination key provided */ - /* invalid proof provided */ - return GNUNET_SYSERR; -} - - -/** - * Verify that the signatures on the "409 CONFLICT" response from the - * exchange demonstrating customer double-spending are valid. - * - * @param mh melt handle - * @param json json reply with the signature(s) and transaction history - * @return #GNUNET_OK if the signature(s) is valid, #GNUNET_SYSERR if not - */ -static int -verify_melt_signature_spend_conflict (struct TALER_EXCHANGE_MeltHandle *mh, - const json_t *json) -{ - json_t *history; - struct TALER_Amount original_value; - struct TALER_Amount melt_value_with_fee; - struct TALER_Amount total; - struct TALER_CoinSpendPublicKeyP coin_pub; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("history", &history), - GNUNET_JSON_spec_fixed_auto ("coin_pub", &coin_pub), - TALER_JSON_spec_amount_any ("original_value", &original_value), - TALER_JSON_spec_amount_any ("requested_value", &melt_value_with_fee), - GNUNET_JSON_spec_end () - }; - const struct MeltedCoin *mc; - enum TALER_ErrorCode ec; - struct GNUNET_HashCode h_denom_pub; - - /* parse JSON reply */ - if (GNUNET_OK != - GNUNET_JSON_parse (json, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - /* Find out which coin was deemed problematic by the exchange */ - mc = &mh->md->melted_coin; - - /* check basic coin properties */ - if (0 != TALER_amount_cmp (&original_value, - &mc->original_value)) - { - /* We disagree on the value of the coin */ - GNUNET_break_op (0); - json_decref (history); - return GNUNET_SYSERR; - } - if (0 != TALER_amount_cmp (&melt_value_with_fee, - &mc->melt_amount_with_fee)) - { - /* We disagree on the value of the coin */ - GNUNET_break_op (0); - json_decref (history); - return GNUNET_SYSERR; - } - - /* verify coin history */ - memset (&h_denom_pub, - 0, - sizeof (h_denom_pub)); - history = json_object_get (json, - "history"); - if (GNUNET_OK != - TALER_EXCHANGE_verify_coin_history (&mh->dki, - original_value.currency, - &coin_pub, - history, - &h_denom_pub, - &total)) - { - GNUNET_break_op (0); - json_decref (history); - return GNUNET_SYSERR; - } - json_decref (history); - - ec = TALER_JSON_get_error_code (json); - switch (ec) - { - case TALER_EC_EXCHANGE_MELT_INSUFFICIENT_FUNDS: - /* check if melt operation was really too expensive given history */ - if (0 > - TALER_amount_add (&total, - &total, - &melt_value_with_fee)) - { - /* clearly not OK if our transaction would have caused - the overflow... */ - return GNUNET_OK; - } - - if (0 >= TALER_amount_cmp (&total, - &original_value)) - { - /* transaction should have still fit */ - GNUNET_break (0); - return GNUNET_SYSERR; - } - - /* everything OK, valid proof of double-spending was provided */ - return GNUNET_OK; - case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: - if (0 != GNUNET_memcmp (&mh->dki.h_key, - &h_denom_pub)) - return GNUNET_OK; /* indeed, proof with different denomination key provided */ - /* invalid proof provided */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - default: - /* unexpected error code */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } -} - - -/** * Function called when we're done processing the * HTTP /coins/$COIN_PUB/melt request. * @@ -333,214 +213,181 @@ handle_melt_finished (void *cls, const void *response) { struct TALER_EXCHANGE_MeltHandle *mh = cls; - uint32_t noreveal_index = TALER_CNC_KAPPA; /* invalid value */ - struct TALER_ExchangePublicKeyP exchange_pub; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_MeltResponse mr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; mh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + mr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: if (GNUNET_OK != verify_melt_signature_ok (mh, j, - &exchange_pub, - &noreveal_index)) + &mr.details.ok.sign_key)) { GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE; - } - if (NULL != mh->melt_cb) - { - mh->melt_cb (mh->melt_cb_cls, - &hr, - noreveal_index, - (0 == hr.http_status) - ? NULL - : &exchange_pub); - mh->melt_cb = NULL; + mr.hr.http_status = 0; + mr.hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE; + break; } + mr.details.ok.noreveal_index = mh->noreveal_index; + mr.details.ok.num_mbds = mh->rd->fresh_pks_len; + mr.details.ok.mbds = mh->mbds; + mh->melt_cb (mh->melt_cb_cls, + &mr); + mh->melt_cb = NULL; 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); + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_CONFLICT: - hr.ec = TALER_JSON_get_error_code (j); - switch (hr.ec) - { - case TALER_EC_EXCHANGE_MELT_INSUFFICIENT_FUNDS: - /* Double spending; check signatures on transaction history */ - if (GNUNET_OK != - verify_melt_signature_spend_conflict (mh, - j)) - { - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE; - hr.hint = TALER_JSON_get_error_hint (j); - } - break; - case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: - if (GNUNET_OK != - verify_melt_signature_denom_conflict (mh, - j)) - { - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE; - hr.hint = TALER_JSON_get_error_hint (j); - } - break; - default: - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE; - hr.hint = TALER_JSON_get_error_hint (j); - break; - } + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_FORBIDDEN: /* Nothing really to verify, exchange says one of the signatures is invalid; assuming 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); + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_NOT_FOUND: /* Nothing really to verify, 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); + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.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); + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); break; default: /* unexpected response code */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange melt\n", (unsigned int) response_code, - hr.ec); + mr.hr.ec); GNUNET_break_op (0); break; } if (NULL != mh->melt_cb) mh->melt_cb (mh->melt_cb_cls, - &hr, - UINT32_MAX, - NULL); + &mr); TALER_EXCHANGE_melt_cancel (mh); } -struct TALER_EXCHANGE_MeltHandle * -TALER_EXCHANGE_melt (struct TALER_EXCHANGE_Handle *exchange, - size_t refresh_data_length, - const char *refresh_data, - TALER_EXCHANGE_MeltCallback melt_cb, - void *melt_cb_cls) +/** + * Start the actual melt operation, now that we have + * the exchange's input values. + * + * @param[in,out] mh melt operation to run + * @return #GNUNET_OK if we could start the operation + */ +static enum GNUNET_GenericReturnValue +start_melt (struct TALER_EXCHANGE_MeltHandle *mh) { const struct TALER_EXCHANGE_Keys *key_state; - const struct TALER_EXCHANGE_DenomPublicKey *dki; json_t *melt_obj; - struct TALER_EXCHANGE_MeltHandle *mh; CURL *eh; - struct GNUNET_CURL_Context *ctx; - struct MeltData *md; - struct TALER_CoinSpendSignatureP confirm_sig; char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32]; - struct TALER_RefreshMeltCoinAffirmationPS melt = { - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_MELT), - .purpose.size = htonl (sizeof (melt)), - }; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_ExchangeWithdrawValues alg_values[mh->rd->fresh_pks_len]; - GNUNET_assert (GNUNET_YES == - TEAH_handle_is_ready (exchange)); - md = TALER_EXCHANGE_deserialize_melt_data_ (refresh_data, - refresh_data_length); - if (NULL == md) + for (unsigned int i = 0; i<mh->rd->fresh_pks_len; i++) + { + if (GNUNET_CRYPTO_BSA_RSA == + mh->rd->fresh_pks[i].key.bsign_pub_key->cipher) + alg_values[i] = *TALER_denom_ewv_rsa_singleton (); + else + alg_values[i] = mh->mbds[i].alg_value; + } + if (GNUNET_OK != + TALER_EXCHANGE_get_melt_data_ (&mh->rms, + mh->rd, + alg_values, + &mh->md)) { GNUNET_break (0); - return NULL; + return GNUNET_SYSERR; } - melt.rc = md->rc; - TALER_amount_hton (&melt.amount_with_fee, - &md->melted_coin.melt_amount_with_fee); - TALER_amount_hton (&melt.melt_fee, - &md->melted_coin.fee_melt); - GNUNET_CRYPTO_eddsa_key_get_public (&md->melted_coin.coin_priv.eddsa_priv, - &melt.coin_pub.eddsa_pub); - GNUNET_CRYPTO_rsa_public_key_hash (md->melted_coin.pub_key.rsa_public_key, - &melt.h_denom_pub); - GNUNET_CRYPTO_eddsa_sign (&md->melted_coin.coin_priv.eddsa_priv, - &melt, - &confirm_sig.eddsa_signature); + TALER_denom_pub_hash (&mh->md.melted_coin.pub_key, + &h_denom_pub); + TALER_wallet_melt_sign ( + &mh->md.melted_coin.melt_amount_with_fee, + &mh->md.melted_coin.fee_melt, + &mh->md.rc, + &h_denom_pub, + mh->md.melted_coin.h_age_commitment, + &mh->md.melted_coin.coin_priv, + &mh->coin_sig); + GNUNET_CRYPTO_eddsa_key_get_public (&mh->md.melted_coin.coin_priv.eddsa_priv, + &mh->coin_pub.eddsa_pub); melt_obj = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("coin_pub", - &melt.coin_pub), GNUNET_JSON_pack_data_auto ("denom_pub_hash", - &melt.h_denom_pub), - TALER_JSON_pack_denomination_signature ("denom_sig", - &md->melted_coin.sig), + &h_denom_pub), + TALER_JSON_pack_denom_sig ("denom_sig", + &mh->md.melted_coin.sig), GNUNET_JSON_pack_data_auto ("confirm_sig", - &confirm_sig), + &mh->coin_sig), TALER_JSON_pack_amount ("value_with_fee", - &md->melted_coin.melt_amount_with_fee), + &mh->md.melted_coin.melt_amount_with_fee), GNUNET_JSON_pack_data_auto ("rc", - &melt.rc)); + &mh->md.rc), + GNUNET_JSON_pack_allow_null ( + (NULL != mh->md.melted_coin.h_age_commitment) + ? GNUNET_JSON_pack_data_auto ("age_commitment_hash", + mh->md.melted_coin.h_age_commitment) + : GNUNET_JSON_pack_string ("age_commitment_hash", + NULL)), + GNUNET_JSON_pack_allow_null ( + mh->send_rms + ? GNUNET_JSON_pack_data_auto ("rms", + &mh->rms) + : GNUNET_JSON_pack_string ("rms", + NULL))); { char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; char *end; end = GNUNET_STRINGS_data_to_string ( - &melt.coin_pub, + &mh->coin_pub, sizeof (struct TALER_CoinSpendPublicKeyP), pub_str, sizeof (pub_str)); *end = '\0'; GNUNET_snprintf (arg_str, sizeof (arg_str), - "/coins/%s/melt", + "coins/%s/melt", pub_str); } - key_state = TALER_EXCHANGE_get_keys (exchange); - dki = TALER_EXCHANGE_get_denomination_key (key_state, - &md->melted_coin.pub_key); + key_state = mh->keys; + mh->dki = TALER_EXCHANGE_get_denomination_key (key_state, + &mh->md.melted_coin.pub_key); /* and now we can at last begin the actual request handling */ - mh = GNUNET_new (struct TALER_EXCHANGE_MeltHandle); - mh->exchange = exchange; - mh->coin_pub = melt.coin_pub; - mh->dki = *dki; - mh->dki.key.rsa_public_key = NULL; /* lifetime not warranted, so better - not copy the pointer */ - mh->melt_cb = melt_cb; - mh->melt_cb_cls = melt_cb_cls; - mh->md = md; - mh->url = TEAH_path_to_url (exchange, - arg_str); + + mh->url = TALER_url_join (mh->exchange_url, + arg_str, + NULL); if (NULL == mh->url) { json_decref (melt_obj); - GNUNET_free (mh); - return NULL; + return GNUNET_SYSERR; } eh = TALER_EXCHANGE_curl_easy_get_ (mh->url); if ( (NULL == eh) || @@ -553,17 +400,176 @@ TALER_EXCHANGE_melt (struct TALER_EXCHANGE_Handle *exchange, if (NULL != eh) curl_easy_cleanup (eh); json_decref (melt_obj); - GNUNET_free (mh->url); - GNUNET_free (mh); - return NULL; + return GNUNET_SYSERR; } json_decref (melt_obj); - ctx = TEAH_handle_to_context (exchange); - mh->job = GNUNET_CURL_job_add2 (ctx, + mh->job = GNUNET_CURL_job_add2 (mh->cctx, eh, mh->ctx.headers, &handle_melt_finished, mh); + return GNUNET_OK; +} + + +/** + * The melt request @a mh failed, return an error to + * the application and cancel the operation. + * + * @param[in] mh melt request that failed + * @param ec error code to fail with + */ +static void +fail_mh (struct TALER_EXCHANGE_MeltHandle *mh, + enum TALER_ErrorCode ec) +{ + struct TALER_EXCHANGE_MeltResponse mr = { + .hr.ec = ec + }; + + mh->melt_cb (mh->melt_cb_cls, + &mr); + TALER_EXCHANGE_melt_cancel (mh); +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * CS R request to a exchange. + * + * @param cls closure with our `struct TALER_EXCHANGE_MeltHandle *` + * @param csrr response details + */ +static void +csr_cb (void *cls, + const struct TALER_EXCHANGE_CsRMeltResponse *csrr) +{ + struct TALER_EXCHANGE_MeltHandle *mh = cls; + unsigned int nks_off = 0; + + mh->csr = NULL; + if (MHD_HTTP_OK != csrr->hr.http_status) + { + struct TALER_EXCHANGE_MeltResponse mr = { + .hr = csrr->hr + }; + + mr.hr.hint = "/csr-melt failed"; + mh->melt_cb (mh->melt_cb_cls, + &mr); + TALER_EXCHANGE_melt_cancel (mh); + return; + } + for (unsigned int i = 0; i<mh->rd->fresh_pks_len; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk = + &mh->rd->fresh_pks[i]; + struct TALER_ExchangeWithdrawValues *wv = &mh->mbds[i].alg_value; + + switch (fresh_pk->key.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_break (0); + fail_mh (mh, + TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR); + return; + case GNUNET_CRYPTO_BSA_RSA: + break; + case GNUNET_CRYPTO_BSA_CS: + TALER_denom_ewv_copy (wv, + &csrr->details.ok.alg_values[nks_off]); + nks_off++; + break; + } + } + mh->send_rms = true; + if (GNUNET_OK != + start_melt (mh)) + { + GNUNET_break (0); + fail_mh (mh, + TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR); + return; + } +} + + +struct TALER_EXCHANGE_MeltHandle * +TALER_EXCHANGE_melt ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_EXCHANGE_RefreshData *rd, + TALER_EXCHANGE_MeltCallback melt_cb, + void *melt_cb_cls) +{ + struct TALER_EXCHANGE_NonceKey nks[GNUNET_NZL (rd->fresh_pks_len)]; + unsigned int nks_off = 0; + struct TALER_EXCHANGE_MeltHandle *mh; + + if (0 == rd->fresh_pks_len) + { + GNUNET_break (0); + return NULL; + } + mh = GNUNET_new (struct TALER_EXCHANGE_MeltHandle); + mh->noreveal_index = TALER_CNC_KAPPA; /* invalid value */ + mh->cctx = ctx; + mh->exchange_url = GNUNET_strdup (url); + mh->rd = rd; + mh->rms = *rms; + mh->melt_cb = melt_cb; + mh->melt_cb_cls = melt_cb_cls; + mh->mbds = GNUNET_new_array (rd->fresh_pks_len, + struct TALER_EXCHANGE_MeltBlindingDetail); + for (unsigned int i = 0; i<rd->fresh_pks_len; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk = &rd->fresh_pks[i]; + + switch (fresh_pk->key.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_break (0); + GNUNET_free (mh->mbds); + GNUNET_free (mh); + return NULL; + case GNUNET_CRYPTO_BSA_RSA: + TALER_denom_ewv_copy (&mh->mbds[i].alg_value, + TALER_denom_ewv_rsa_singleton ()); + break; + case GNUNET_CRYPTO_BSA_CS: + nks[nks_off].pk = fresh_pk; + nks[nks_off].cnc_num = nks_off; + nks_off++; + break; + } + } + mh->keys = TALER_EXCHANGE_keys_incref (keys); + if (0 != nks_off) + { + mh->csr = TALER_EXCHANGE_csr_melt (ctx, + url, + rms, + nks_off, + nks, + &csr_cb, + mh); + if (NULL == mh->csr) + { + GNUNET_break (0); + TALER_EXCHANGE_melt_cancel (mh); + return NULL; + } + return mh; + } + if (GNUNET_OK != + start_melt (mh)) + { + GNUNET_break (0); + TALER_EXCHANGE_melt_cancel (mh); + return NULL; + } return mh; } @@ -571,15 +577,24 @@ TALER_EXCHANGE_melt (struct TALER_EXCHANGE_Handle *exchange, void TALER_EXCHANGE_melt_cancel (struct TALER_EXCHANGE_MeltHandle *mh) { + for (unsigned int i = 0; i<mh->rd->fresh_pks_len; i++) + TALER_denom_ewv_free (&mh->mbds[i].alg_value); if (NULL != mh->job) { GNUNET_CURL_job_cancel (mh->job); mh->job = NULL; } - TALER_EXCHANGE_free_melt_data_ (mh->md); /* does not free 'md' itself */ - GNUNET_free (mh->md); + if (NULL != mh->csr) + { + TALER_EXCHANGE_csr_melt_cancel (mh->csr); + mh->csr = NULL; + } + TALER_EXCHANGE_free_melt_data_ (&mh->md); /* does not free 'md' itself */ + GNUNET_free (mh->mbds); GNUNET_free (mh->url); + GNUNET_free (mh->exchange_url); TALER_curl_easy_post_finished (&mh->ctx); + TALER_EXCHANGE_keys_decref (mh->keys); GNUNET_free (mh); } diff --git a/src/lib/exchange_api_purse_create_with_deposit.c b/src/lib/exchange_api_purse_create_with_deposit.c new file mode 100644 index 000000000..fff898e57 --- /dev/null +++ b/src/lib/exchange_api_purse_create_with_deposit.c @@ -0,0 +1,656 @@ +/* + This file is part of TALER + Copyright (C) 2022-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 + <http://www.gnu.org/licenses/> + */ +/** + * @file lib/exchange_api_purse_create_with_deposit.c + * @brief Implementation of the client to create a purse with + * an initial set of deposits (and a contract) + * @author Christian Grothoff + */ +#include "platform.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 "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_handle.h" +#include "exchange_api_common.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * Information we track per deposited coin. + */ +struct Deposit +{ + /** + * Coin's public key. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature made with the coin. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Coin's denomination. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Age restriction hash for the coin. + */ + struct TALER_AgeCommitmentHash ahac; + + /** + * How much did we say the coin contributed. + */ + struct TALER_Amount contribution; +}; + + +/** + * @brief A purse create with deposit handle + */ +struct TALER_EXCHANGE_PurseCreateDepositHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * The base URL of the exchange. + */ + char *exchange_url; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_PurseCreateDepositCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Expected value in the purse after fees. + */ + struct TALER_Amount purse_value_after_fees; + + /** + * Our encrypted contract (if we had any). + */ + struct TALER_EncryptedContract econtract; + + /** + * Public key of the merge capability. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Public key of the purse. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * Signature with the purse key on the request. + */ + struct TALER_PurseContractSignatureP purse_sig; + + /** + * Hash over the purse's contract terms. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * When does the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * Array of @e num_deposit deposits. + */ + struct Deposit *deposits; + + /** + * How many deposits did we make? + */ + unsigned int num_deposits; + +}; + + +/** + * Function called when we're done processing the + * HTTP /deposit request. + * + * @param cls the `struct TALER_EXCHANGE_DepositHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_purse_create_deposit_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_PurseCreateDepositHandle *pch = cls; + const json_t *j = response; + struct TALER_EXCHANGE_PurseCreateDepositResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + const struct TALER_EXCHANGE_Keys *keys = pch->keys; + + pch->job = NULL; + switch (response_code) + { + case 0: + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + struct GNUNET_TIME_Timestamp etime; + struct TALER_Amount total_deposited; + struct TALER_ExchangeSignatureP exchange_sig; + struct TALER_ExchangePublicKeyP exchange_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_spec_timestamp ("exchange_timestamp", + &etime), + TALER_JSON_spec_amount ("total_deposited", + pch->purse_value_after_fees.currency, + &total_deposited), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (keys, + &exchange_pub)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID; + break; + } + if (GNUNET_OK != + TALER_exchange_online_purse_created_verify ( + etime, + pch->purse_expiration, + &pch->purse_value_after_fees, + &total_deposited, + &pch->purse_pub, + &pch->h_contract_terms, + &exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID; + 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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_CONFLICT: + { + dr.hr.ec = TALER_JSON_get_error_code (j); + switch (dr.hr.ec) + { + case TALER_EC_EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA: + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_create_conflict_ ( + &pch->purse_sig, + &pch->purse_pub, + j)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: + /* Nothing to check anymore here, proof needs to be + checked in the GET /coins/$COIN_PUB handler */ + break; + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + // FIXME #7267: write check (add to exchange_api_common! */ + break; + case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA: + { + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_AgeCommitmentHash phac; + bool found = false; + + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_coin_conflict_ ( + &pch->purse_pub, + pch->exchange_url, + j, + &h_denom_pub, + &phac, + &coin_pub, + &coin_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + for (unsigned int i = 0; i<pch->num_deposits; i++) + { + struct Deposit *deposit = &pch->deposits[i]; + + if (0 != + GNUNET_memcmp (&coin_pub, + &deposit->coin_pub)) + continue; + if (0 != + GNUNET_memcmp (&deposit->h_denom_pub, + &h_denom_pub)) + { + found = true; + break; + } + if (0 != + GNUNET_memcmp (&deposit->ahac, + &phac)) + { + found = true; + break; + } + if (0 == + GNUNET_memcmp (&coin_sig, + &deposit->coin_sig)) + { + GNUNET_break_op (0); + continue; + } + found = true; + break; + } + if (! found) + { + /* conflict is for a different coin! */ + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + case TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA: + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_econtract_conflict_ ( + &pch->econtract.econtract_sig, + &pch->purse_pub, + j)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected error code %d for conflcting deposit\n", + dr.hr.ec); + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + } + 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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange deposit\n", + (unsigned int) response_code, + dr.hr.ec); + GNUNET_break_op (0); + break; + } + pch->cb (pch->cb_cls, + &dr); + TALER_EXCHANGE_purse_create_with_deposit_cancel (pch); +} + + +struct TALER_EXCHANGE_PurseCreateDepositHandle * +TALER_EXCHANGE_purse_create_with_deposit ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_PurseContractPrivateKeyP *purse_priv, + const struct TALER_PurseMergePrivateKeyP *merge_priv, + const struct TALER_ContractDiffiePrivateP *contract_priv, + const json_t *contract_terms, + unsigned int num_deposits, + const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits], + bool upload_contract, + TALER_EXCHANGE_PurseCreateDepositCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_PurseCreateDepositHandle *pch; + json_t *create_obj; + json_t *deposit_arr; + CURL *eh; + char arg_str[sizeof (pch->purse_pub) * 2 + 32]; + uint32_t min_age = 0; + + pch = GNUNET_new (struct TALER_EXCHANGE_PurseCreateDepositHandle); + pch->cb = cb; + pch->cb_cls = cb_cls; + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("pay_deadline", + &pch->purse_expiration), + TALER_JSON_spec_amount_any ("amount", + &pch->purse_value_after_fees), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("minimum_age", + &min_age), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (contract_terms, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + } + if (GNUNET_OK != + TALER_JSON_contract_hash (contract_terms, + &pch->h_contract_terms)) + { + GNUNET_break (0); + return NULL; + } + GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv, + &pch->purse_pub.eddsa_pub); + { + char pub_str[sizeof (pch->purse_pub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &pch->purse_pub, + sizeof (pch->purse_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "purses/%s/create", + pub_str); + } + GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv, + &pch->merge_pub.eddsa_pub); + pch->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == pch->url) + { + GNUNET_break (0); + GNUNET_free (pch); + return NULL; + } + pch->num_deposits = num_deposits; + pch->deposits = GNUNET_new_array (num_deposits, + struct Deposit); + deposit_arr = json_array (); + GNUNET_assert (NULL != deposit_arr); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signing with URL `%s'\n", + url); + for (unsigned int i = 0; i<num_deposits; i++) + { + const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i]; + const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof; + struct Deposit *d = &pch->deposits[i]; + json_t *jdeposit; + struct TALER_AgeCommitmentHash *aghp = NULL; + struct TALER_AgeAttestation attest; + struct TALER_AgeAttestation *attestp = NULL; + + if (NULL != acp) + { + TALER_age_commitment_hash (&acp->commitment, + &d->ahac); + aghp = &d->ahac; + if (GNUNET_OK != + TALER_age_commitment_attest (acp, + min_age, + &attest)) + { + GNUNET_break (0); + GNUNET_array_grow (pch->deposits, + pch->num_deposits, + 0); + GNUNET_free (pch->url); + json_decref (deposit_arr); + GNUNET_free (pch); + return NULL; + } + } + d->contribution = deposit->amount; + d->h_denom_pub = deposit->h_denom_pub; + GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, + &d->coin_pub.eddsa_pub); + TALER_wallet_purse_deposit_sign ( + url, + &pch->purse_pub, + &deposit->amount, + &d->h_denom_pub, + &d->ahac, + &deposit->coin_priv, + &d->coin_sig); + jdeposit = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + aghp)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("age_attestation", + attestp)), + TALER_JSON_pack_amount ("amount", + &deposit->amount), + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &deposit->h_denom_pub), + TALER_JSON_pack_denom_sig ("ub_sig", + &deposit->denom_sig), + GNUNET_JSON_pack_data_auto ("coin_sig", + &d->coin_sig), + GNUNET_JSON_pack_data_auto ("coin_pub", + &d->coin_pub)); + GNUNET_assert (0 == + json_array_append_new (deposit_arr, + jdeposit)); + } + TALER_wallet_purse_create_sign (pch->purse_expiration, + &pch->h_contract_terms, + &pch->merge_pub, + min_age, + &pch->purse_value_after_fees, + purse_priv, + &pch->purse_sig); + if (upload_contract) + { + TALER_CRYPTO_contract_encrypt_for_merge (&pch->purse_pub, + contract_priv, + merge_priv, + contract_terms, + &pch->econtract.econtract, + &pch->econtract.econtract_size); + GNUNET_CRYPTO_ecdhe_key_get_public (&contract_priv->ecdhe_priv, + &pch->econtract.contract_pub.ecdhe_pub); + TALER_wallet_econtract_upload_sign (pch->econtract.econtract, + pch->econtract.econtract_size, + &pch->econtract.contract_pub, + purse_priv, + &pch->econtract.econtract_sig); + } + create_obj = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &pch->purse_value_after_fees), + GNUNET_JSON_pack_uint64 ("min_age", + min_age), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_econtract ("econtract", + upload_contract + ? &pch->econtract + : NULL)), + GNUNET_JSON_pack_data_auto ("purse_sig", + &pch->purse_sig), + GNUNET_JSON_pack_data_auto ("merge_pub", + &pch->merge_pub), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &pch->h_contract_terms), + GNUNET_JSON_pack_timestamp ("purse_expiration", + pch->purse_expiration), + GNUNET_JSON_pack_array_steal ("deposits", + deposit_arr)); + GNUNET_assert (NULL != create_obj); + eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&pch->ctx, + eh, + create_obj)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (create_obj); + GNUNET_free (pch->econtract.econtract); + GNUNET_array_grow (pch->deposits, + pch->num_deposits, + 0); + GNUNET_free (pch->url); + GNUNET_free (pch); + return NULL; + } + json_decref (create_obj); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "URL for purse create with deposit: `%s'\n", + pch->url); + pch->keys = TALER_EXCHANGE_keys_incref (keys); + pch->exchange_url = GNUNET_strdup (url); + pch->job = GNUNET_CURL_job_add2 (ctx, + eh, + pch->ctx.headers, + &handle_purse_create_deposit_finished, + pch); + return pch; +} + + +void +TALER_EXCHANGE_purse_create_with_deposit_cancel ( + struct TALER_EXCHANGE_PurseCreateDepositHandle *pch) +{ + if (NULL != pch->job) + { + GNUNET_CURL_job_cancel (pch->job); + pch->job = NULL; + } + GNUNET_free (pch->econtract.econtract); + GNUNET_free (pch->exchange_url); + GNUNET_free (pch->url); + GNUNET_array_grow (pch->deposits, + pch->num_deposits, + 0); + TALER_EXCHANGE_keys_decref (pch->keys); + TALER_curl_easy_post_finished (&pch->ctx); + GNUNET_free (pch); +} + + +/* end of exchange_api_purse_create_with_deposit.c */ diff --git a/src/lib/exchange_api_purse_create_with_merge.c b/src/lib/exchange_api_purse_create_with_merge.c new file mode 100644 index 000000000..0c8878342 --- /dev/null +++ b/src/lib/exchange_api_purse_create_with_merge.c @@ -0,0 +1,580 @@ +/* + This file is part of TALER + Copyright (C) 2022-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 + <http://www.gnu.org/licenses/> + */ +/** + * @file lib/exchange_api_purse_create_with_merge.c + * @brief Implementation of the client to create a + * purse for an account + * @author Christian Grothoff + */ +#include "platform.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 "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_handle.h" +#include "exchange_api_common.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * @brief A purse create with merge handle + */ +struct TALER_EXCHANGE_PurseCreateMergeHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * The exchange base URL. + */ + char *exchange_url; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_PurseCreateMergeCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * The encrypted contract (if any). + */ + struct TALER_EncryptedContract econtract; + + /** + * Expected value in the purse after fees. + */ + struct TALER_Amount purse_value_after_fees; + + /** + * Public key of the reserve public key. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Reserve signature affirming our merge. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Merge capability key. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Our merge signature (if any). + */ + struct TALER_PurseMergeSignatureP merge_sig; + + /** + * Public key of the purse. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * Request data we signed over. + */ + struct TALER_PurseContractSignatureP purse_sig; + + /** + * Hash over the purse's contrac terms. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * When does the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * When does the purse get merged/created. + */ + struct GNUNET_TIME_Timestamp merge_timestamp; +}; + + +/** + * Function called when we're done processing the + * HTTP /reserves/$RID/purse request. + * + * @param cls the `struct TALER_EXCHANGE_PurseCreateMergeHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_purse_create_with_merge_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm = cls; + const json_t *j = response; + struct TALER_EXCHANGE_PurseCreateMergeResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code, + .reserve_sig = &pcm->reserve_sig + }; + + pcm->job = NULL; + switch (response_code) + { + case 0: + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + struct GNUNET_TIME_Timestamp etime; + struct TALER_Amount total_deposited; + struct TALER_ExchangeSignatureP exchange_sig; + struct TALER_ExchangePublicKeyP exchange_pub; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("total_deposited", + &total_deposited), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_spec_timestamp ("exchange_timestamp", + &etime), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (pcm->keys, + &exchange_pub)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (GNUNET_OK != + TALER_exchange_online_purse_created_verify ( + etime, + pcm->purse_expiration, + &pcm->purse_value_after_fees, + &total_deposited, + &pcm->purse_pub, + &pcm->h_contract_terms, + &exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_CONFLICT: + dr.hr.ec = TALER_JSON_get_error_code (j); + switch (dr.hr.ec) + { + case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA: + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_create_conflict_ ( + &pcm->purse_sig, + &pcm->purse_pub, + j)) + { + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + case TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA: + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_merge_conflict_ ( + &pcm->merge_sig, + &pcm->merge_pub, + &pcm->purse_pub, + pcm->exchange_url, + j)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS: + /* nothing to verify */ + break; + case TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA: + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_econtract_conflict_ ( + &pcm->econtract.econtract_sig, + &pcm->purse_pub, + j)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + default: + /* unexpected EC! */ + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } /* end inner (EC) switch */ + 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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &dr.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange deposit\n", + (unsigned int) response_code, + dr.hr.ec); + GNUNET_break_op (0); + break; + } + pcm->cb (pcm->cb_cls, + &dr); + TALER_EXCHANGE_purse_create_with_merge_cancel (pcm); +} + + +struct TALER_EXCHANGE_PurseCreateMergeHandle * +TALER_EXCHANGE_purse_create_with_merge ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_PurseContractPrivateKeyP *purse_priv, + const struct TALER_PurseMergePrivateKeyP *merge_priv, + const struct TALER_ContractDiffiePrivateP *contract_priv, + const json_t *contract_terms, + bool upload_contract, + bool pay_for_purse, + struct GNUNET_TIME_Timestamp merge_timestamp, + TALER_EXCHANGE_PurseCreateMergeCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm; + json_t *create_with_merge_obj; + CURL *eh; + char arg_str[sizeof (pcm->reserve_pub) * 2 + 32]; + uint32_t min_age = 0; + struct TALER_Amount purse_fee; + enum TALER_WalletAccountMergeFlags flags; + + pcm = GNUNET_new (struct TALER_EXCHANGE_PurseCreateMergeHandle); + pcm->cb = cb; + pcm->cb_cls = cb_cls; + if (GNUNET_OK != + TALER_JSON_contract_hash (contract_terms, + &pcm->h_contract_terms)) + { + GNUNET_break (0); + GNUNET_free (pcm); + return NULL; + } + pcm->merge_timestamp = merge_timestamp; + GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv, + &pcm->purse_pub.eddsa_pub); + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &pcm->reserve_pub.eddsa_pub); + GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv, + &pcm->merge_pub.eddsa_pub); + + { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("amount", + &pcm->purse_value_after_fees), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("minimum_age", + &min_age), + NULL), + GNUNET_JSON_spec_timestamp ("pay_deadline", + &pcm->purse_expiration), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (contract_terms, + spec, + NULL, NULL)) + { + GNUNET_break (0); + GNUNET_free (pcm); + return NULL; + } + } + if (pay_for_purse) + { + const struct TALER_EXCHANGE_GlobalFee *gf; + + gf = TALER_EXCHANGE_get_global_fee ( + keys, + GNUNET_TIME_timestamp_get ()); + purse_fee = gf->fees.purse; + flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; + } + else + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (pcm->purse_value_after_fees.currency, + &purse_fee)); + flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; + } + + { + char pub_str[sizeof (pcm->reserve_pub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &pcm->reserve_pub, + sizeof (pcm->reserve_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves/%s/purse", + pub_str); + } + pcm->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == pcm->url) + { + GNUNET_break (0); + GNUNET_free (pcm); + return NULL; + } + TALER_wallet_purse_create_sign (pcm->purse_expiration, + &pcm->h_contract_terms, + &pcm->merge_pub, + min_age, + &pcm->purse_value_after_fees, + purse_priv, + &pcm->purse_sig); + { + char *payto_uri; + + payto_uri = TALER_reserve_make_payto (url, + &pcm->reserve_pub); + TALER_wallet_purse_merge_sign (payto_uri, + merge_timestamp, + &pcm->purse_pub, + merge_priv, + &pcm->merge_sig); + GNUNET_free (payto_uri); + } + TALER_wallet_account_merge_sign (merge_timestamp, + &pcm->purse_pub, + pcm->purse_expiration, + &pcm->h_contract_terms, + &pcm->purse_value_after_fees, + &purse_fee, + min_age, + flags, + reserve_priv, + &pcm->reserve_sig); + if (upload_contract) + { + TALER_CRYPTO_contract_encrypt_for_deposit ( + &pcm->purse_pub, + contract_priv, + contract_terms, + &pcm->econtract.econtract, + &pcm->econtract.econtract_size); + GNUNET_CRYPTO_ecdhe_key_get_public (&contract_priv->ecdhe_priv, + &pcm->econtract.contract_pub.ecdhe_pub); + TALER_wallet_econtract_upload_sign ( + pcm->econtract.econtract, + pcm->econtract.econtract_size, + &pcm->econtract.contract_pub, + purse_priv, + &pcm->econtract.econtract_sig); + } + create_with_merge_obj = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("purse_value", + &pcm->purse_value_after_fees), + GNUNET_JSON_pack_uint64 ("min_age", + min_age), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_econtract ("econtract", + upload_contract + ? &pcm->econtract + : NULL)), + GNUNET_JSON_pack_allow_null ( + pay_for_purse + ? TALER_JSON_pack_amount ("purse_fee", + &purse_fee) + : GNUNET_JSON_pack_string ("dummy2", + NULL)), + GNUNET_JSON_pack_data_auto ("merge_pub", + &pcm->merge_pub), + GNUNET_JSON_pack_data_auto ("merge_sig", + &pcm->merge_sig), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &pcm->reserve_sig), + GNUNET_JSON_pack_data_auto ("purse_pub", + &pcm->purse_pub), + GNUNET_JSON_pack_data_auto ("purse_sig", + &pcm->purse_sig), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &pcm->h_contract_terms), + GNUNET_JSON_pack_timestamp ("merge_timestamp", + merge_timestamp), + GNUNET_JSON_pack_timestamp ("purse_expiration", + pcm->purse_expiration)); + GNUNET_assert (NULL != create_with_merge_obj); + eh = TALER_EXCHANGE_curl_easy_get_ (pcm->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&pcm->ctx, + eh, + create_with_merge_obj)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (create_with_merge_obj); + GNUNET_free (pcm->econtract.econtract); + GNUNET_free (pcm->url); + GNUNET_free (pcm); + return NULL; + } + json_decref (create_with_merge_obj); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "URL for purse create_with_merge: `%s'\n", + pcm->url); + pcm->keys = TALER_EXCHANGE_keys_incref (keys); + pcm->exchange_url = GNUNET_strdup (url); + pcm->job = GNUNET_CURL_job_add2 (ctx, + eh, + pcm->ctx.headers, + &handle_purse_create_with_merge_finished, + pcm); + return pcm; +} + + +void +TALER_EXCHANGE_purse_create_with_merge_cancel ( + struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm) +{ + if (NULL != pcm->job) + { + GNUNET_CURL_job_cancel (pcm->job); + pcm->job = NULL; + } + GNUNET_free (pcm->url); + GNUNET_free (pcm->exchange_url); + TALER_curl_easy_post_finished (&pcm->ctx); + TALER_EXCHANGE_keys_decref (pcm->keys); + GNUNET_free (pcm->econtract.econtract); + GNUNET_free (pcm); +} + + +/* end of exchange_api_purse_create_with_merge.c */ diff --git a/src/lib/exchange_api_purse_delete.c b/src/lib/exchange_api_purse_delete.c new file mode 100644 index 000000000..6f8ecc381 --- /dev/null +++ b/src/lib/exchange_api_purse_delete.c @@ -0,0 +1,243 @@ +/* + This file is part of TALER + Copyright (C) 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 + <http://www.gnu.org/licenses/> + */ +/** + * @file lib/exchange_api_purse_delete.c + * @brief Implementation of the client to delete a purse + * into an account + * @author Christian Grothoff + */ +#include "platform.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 "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_handle.h" +#include "exchange_api_common.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * @brief A purse delete with deposit handle + */ +struct TALER_EXCHANGE_PurseDeleteHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_PurseDeleteCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Header with the purse_sig. + */ + struct curl_slist *xhdr; +}; + + +/** + * Function called when we're done processing the + * HTTP DELETE /purse/$PID request. + * + * @param cls the `struct TALER_EXCHANGE_PurseDeleteHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_purse_delete_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_PurseDeleteHandle *pdh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_PurseDeleteResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + pdh->job = NULL; + switch (response_code) + { + case 0: + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_NO_CONTENT: + 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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_CONFLICT: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange deposit\n", + (unsigned int) response_code, + dr.hr.ec); + GNUNET_break_op (0); + break; + } + pdh->cb (pdh->cb_cls, + &dr); + TALER_EXCHANGE_purse_delete_cancel (pdh); +} + + +struct TALER_EXCHANGE_PurseDeleteHandle * +TALER_EXCHANGE_purse_delete ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_PurseContractPrivateKeyP *purse_priv, + TALER_EXCHANGE_PurseDeleteCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_PurseDeleteHandle *pdh; + CURL *eh; + struct TALER_PurseContractPublicKeyP purse_pub; + struct TALER_PurseContractSignatureP purse_sig; + char arg_str[sizeof (purse_pub) * 2 + 32]; + + pdh = GNUNET_new (struct TALER_EXCHANGE_PurseDeleteHandle); + pdh->cb = cb; + pdh->cb_cls = cb_cls; + GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv, + &purse_pub.eddsa_pub); + { + char pub_str[sizeof (purse_pub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string (&purse_pub, + sizeof (purse_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "purses/%s", + pub_str); + } + pdh->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == pdh->url) + { + GNUNET_break (0); + GNUNET_free (pdh); + return NULL; + } + TALER_wallet_purse_delete_sign (purse_priv, + &purse_sig); + { + char *delete_str; + char *xhdr; + + delete_str = + GNUNET_STRINGS_data_to_string_alloc (&purse_sig, + sizeof (purse_sig)); + GNUNET_asprintf (&xhdr, + "Taler-Purse-Signature: %s", + delete_str); + GNUNET_free (delete_str); + pdh->xhdr = curl_slist_append (NULL, + xhdr); + GNUNET_free (xhdr); + } + eh = TALER_EXCHANGE_curl_easy_get_ (pdh->url); + if (NULL == eh) + { + GNUNET_break (0); + curl_slist_free_all (pdh->xhdr); + GNUNET_free (pdh->url); + GNUNET_free (pdh); + return NULL; + } + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + MHD_HTTP_METHOD_DELETE)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "URL for purse delete: `%s'\n", + pdh->url); + pdh->job = GNUNET_CURL_job_add2 (ctx, + eh, + pdh->xhdr, + &handle_purse_delete_finished, + pdh); + return pdh; +} + + +void +TALER_EXCHANGE_purse_delete_cancel ( + struct TALER_EXCHANGE_PurseDeleteHandle *pdh) +{ + if (NULL != pdh->job) + { + GNUNET_CURL_job_cancel (pdh->job); + pdh->job = NULL; + } + curl_slist_free_all (pdh->xhdr); + GNUNET_free (pdh->url); + GNUNET_free (pdh); +} + + +/* end of exchange_api_purse_delete.c */ diff --git a/src/lib/exchange_api_purse_deposit.c b/src/lib/exchange_api_purse_deposit.c new file mode 100644 index 000000000..9c5fa4e78 --- /dev/null +++ b/src/lib/exchange_api_purse_deposit.c @@ -0,0 +1,520 @@ +/* + This file is part of TALER + Copyright (C) 2022-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 + <http://www.gnu.org/licenses/> + */ +/** + * @file lib/exchange_api_purse_deposit.c + * @brief Implementation of the client to create a purse with + * an initial set of deposits (and a contract) + * @author Christian Grothoff + */ +#include "platform.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 "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" + + +/** + * Information we track per coin. + */ +struct Coin +{ + /** + * Coin's public key. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature made with the coin. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Coin's denomination. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Age restriction hash for the coin. + */ + struct TALER_AgeCommitmentHash ahac; + + /** + * How much did we say the coin contributed. + */ + struct TALER_Amount contribution; +}; + + +/** + * @brief A purse create with deposit handle + */ +struct TALER_EXCHANGE_PurseDepositHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * The base url of the exchange we are talking to. + */ + char *base_url; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_PurseDepositCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Public key of the purse. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * Array of @e num_deposits coins we are depositing. + */ + struct Coin *coins; + + /** + * Number of coins we are depositing. + */ + unsigned int num_deposits; +}; + + +/** + * Function called when we're done processing the + * HTTP /purses/$PID/deposit request. + * + * @param cls the `struct TALER_EXCHANGE_PurseDepositHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_purse_deposit_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_PurseDepositHandle *pch = cls; + const json_t *j = response; + struct TALER_EXCHANGE_PurseDepositResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + const struct TALER_EXCHANGE_Keys *keys = pch->keys; + + pch->job = NULL; + switch (response_code) + { + case 0: + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + struct GNUNET_TIME_Timestamp etime; + struct TALER_ExchangeSignatureP exchange_sig; + struct TALER_ExchangePublicKeyP exchange_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &dr.details.ok.h_contract_terms), + GNUNET_JSON_spec_timestamp ("exchange_timestamp", + &etime), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &dr.details.ok.purse_expiration), + TALER_JSON_spec_amount ("total_deposited", + keys->currency, + &dr.details.ok.total_deposited), + TALER_JSON_spec_amount ("purse_value_after_fees", + keys->currency, + &dr.details.ok.purse_value_after_fees), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (keys, + &exchange_pub)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID; + break; + } + if (GNUNET_OK != + TALER_exchange_online_purse_created_verify ( + etime, + dr.details.ok.purse_expiration, + &dr.details.ok.purse_value_after_fees, + &dr.details.ok.total_deposited, + &pch->purse_pub, + &dr.details.ok.h_contract_terms, + &exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID; + 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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr.hr.ec = TALER_JSON_get_error_code (j); + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_CONFLICT: + dr.hr.ec = TALER_JSON_get_error_code (j); + switch (dr.hr.ec) + { + case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA: + { + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_AgeCommitmentHash phac; + bool found = false; + + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_coin_conflict_ ( + &pch->purse_pub, + pch->base_url, + j, + &h_denom_pub, + &phac, + &coin_pub, + &coin_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + for (unsigned int i = 0; i<pch->num_deposits; i++) + { + struct Coin *coin = &pch->coins[i]; + if (0 != GNUNET_memcmp (&coin_pub, + &coin->coin_pub)) + continue; + if (0 != + GNUNET_memcmp (&coin->h_denom_pub, + &h_denom_pub)) + { + found = true; + break; + } + if (0 != + GNUNET_memcmp (&coin->ahac, + &phac)) + { + found = true; + break; + } + if (0 == GNUNET_memcmp (&coin_sig, + &coin->coin_sig)) + { + /* identical signature => not a conflict */ + continue; + } + found = true; + break; + } + if (! found) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + /* meta data conflict is real! */ + break; + } + case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: + /* Nothing to check anymore here, proof needs to be + checked in the GET /coins/$COIN_PUB handler */ + break; + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + break; + default: + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } /* ec switch */ + break; + case MHD_HTTP_GONE: + /* could happen if denomination was revoked or purse expired */ + /* Note: one might want to check /keys for revocation + signature here, alas tricky in case our /keys + is outdated => left to clients */ + dr.hr.ec = TALER_JSON_get_error_code (j); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr.hr.ec = TALER_JSON_get_error_code (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr.hr.ec = TALER_JSON_get_error_code (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange deposit\n", + (unsigned int) response_code, + dr.hr.ec); + GNUNET_break_op (0); + break; + } + if (TALER_EC_NONE == dr.hr.ec) + dr.hr.hint = NULL; + else + dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec); + pch->cb (pch->cb_cls, + &dr); + TALER_EXCHANGE_purse_deposit_cancel (pch); +} + + +struct TALER_EXCHANGE_PurseDepositHandle * +TALER_EXCHANGE_purse_deposit ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const char *purse_exchange_url, + const struct TALER_PurseContractPublicKeyP *purse_pub, + uint8_t min_age, + unsigned int num_deposits, + const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits], + TALER_EXCHANGE_PurseDepositCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_PurseDepositHandle *pch; + json_t *create_obj; + json_t *deposit_arr; + CURL *eh; + char arg_str[sizeof (pch->purse_pub) * 2 + 32]; + + // FIXME: use purse_exchange_url for wad transfers (#7271) + (void) purse_exchange_url; + if (0 == num_deposits) + { + GNUNET_break (0); + return NULL; + } + pch = GNUNET_new (struct TALER_EXCHANGE_PurseDepositHandle); + pch->purse_pub = *purse_pub; + pch->cb = cb; + pch->cb_cls = cb_cls; + { + char pub_str[sizeof (pch->purse_pub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &pch->purse_pub, + sizeof (pch->purse_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "purses/%s/deposit", + pub_str); + } + pch->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == pch->url) + { + GNUNET_break (0); + GNUNET_free (pch); + return NULL; + } + deposit_arr = json_array (); + GNUNET_assert (NULL != deposit_arr); + pch->base_url = GNUNET_strdup (url); + pch->num_deposits = num_deposits; + pch->coins = GNUNET_new_array (num_deposits, + struct Coin); + for (unsigned int i = 0; i<num_deposits; i++) + { + const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i]; + const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof; + struct Coin *coin = &pch->coins[i]; + json_t *jdeposit; + struct TALER_AgeCommitmentHash *achp = NULL; + struct TALER_AgeAttestation attest; + struct TALER_AgeAttestation *attestp = NULL; + + if (NULL != acp) + { + TALER_age_commitment_hash (&acp->commitment, + &coin->ahac); + achp = &coin->ahac; + if (GNUNET_OK != + TALER_age_commitment_attest (acp, + min_age, + &attest)) + { + GNUNET_break (0); + json_decref (deposit_arr); + GNUNET_free (pch->base_url); + GNUNET_free (pch->coins); + GNUNET_free (pch->url); + GNUNET_free (pch); + return NULL; + } + attestp = &attest; + } + GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, + &coin->coin_pub.eddsa_pub); + coin->h_denom_pub = deposit->h_denom_pub; + coin->contribution = deposit->amount; + TALER_wallet_purse_deposit_sign ( + pch->base_url, + &pch->purse_pub, + &deposit->amount, + &coin->h_denom_pub, + &coin->ahac, + &deposit->coin_priv, + &coin->coin_sig); + jdeposit = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + achp)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("age_attestation", + attestp)), + TALER_JSON_pack_amount ("amount", + &deposit->amount), + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &deposit->h_denom_pub), + TALER_JSON_pack_denom_sig ("ub_sig", + &deposit->denom_sig), + GNUNET_JSON_pack_data_auto ("coin_pub", + &coin->coin_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &coin->coin_sig)); + GNUNET_assert (0 == + json_array_append_new (deposit_arr, + jdeposit)); + } + create_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("deposits", + deposit_arr)); + GNUNET_assert (NULL != create_obj); + eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&pch->ctx, + eh, + create_obj)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (create_obj); + GNUNET_free (pch->base_url); + GNUNET_free (pch->url); + GNUNET_free (pch->coins); + GNUNET_free (pch); + return NULL; + } + json_decref (create_obj); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "URL for purse deposit: `%s'\n", + pch->url); + pch->keys = TALER_EXCHANGE_keys_incref (keys); + pch->job = GNUNET_CURL_job_add2 (ctx, + eh, + pch->ctx.headers, + &handle_purse_deposit_finished, + pch); + return pch; +} + + +void +TALER_EXCHANGE_purse_deposit_cancel ( + struct TALER_EXCHANGE_PurseDepositHandle *pch) +{ + if (NULL != pch->job) + { + GNUNET_CURL_job_cancel (pch->job); + pch->job = NULL; + } + GNUNET_free (pch->base_url); + GNUNET_free (pch->url); + GNUNET_free (pch->coins); + TALER_EXCHANGE_keys_decref (pch->keys); + TALER_curl_easy_post_finished (&pch->ctx); + GNUNET_free (pch); +} + + +/* end of exchange_api_purse_deposit.c */ diff --git a/src/lib/exchange_api_purse_merge.c b/src/lib/exchange_api_purse_merge.c new file mode 100644 index 000000000..c013b29d2 --- /dev/null +++ b/src/lib/exchange_api_purse_merge.c @@ -0,0 +1,454 @@ +/* + This file is part of TALER + Copyright (C) 2022-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 + <http://www.gnu.org/licenses/> + */ +/** + * @file lib/exchange_api_purse_merge.c + * @brief Implementation of the client to merge a purse + * into an account + * @author Christian Grothoff + */ +#include "platform.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 "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_handle.h" +#include "exchange_api_common.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * @brief A purse merge with deposit handle + */ +struct TALER_EXCHANGE_AccountMergeHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_AccountMergeCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Base URL of the provider hosting the @e reserve_pub. + */ + char *provider_url; + + /** + * Signature for our operation. + */ + struct TALER_PurseMergeSignatureP merge_sig; + + /** + * Expected value in the purse after fees. + */ + struct TALER_Amount purse_value_after_fees; + + /** + * Public key of the reserve public key. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Public key of the purse. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * Hash over the purse's contrac terms. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * When does the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * Our merge key. + */ + struct TALER_PurseMergePrivateKeyP merge_priv; + + /** + * Reserve signature affirming the merge. + */ + struct TALER_ReserveSignatureP reserve_sig; + +}; + + +/** + * Function called when we're done processing the + * HTTP /purse/$PID/merge request. + * + * @param cls the `struct TALER_EXCHANGE_AccountMergeHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_purse_merge_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_AccountMergeHandle *pch = cls; + const json_t *j = response; + struct TALER_EXCHANGE_AccountMergeResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code, + .reserve_sig = &pch->reserve_sig + }; + + pch->job = NULL; + switch (response_code) + { + case 0: + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + struct TALER_Amount total_deposited; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &dr.details.ok.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &dr.details.ok.exchange_pub), + GNUNET_JSON_spec_timestamp ("exchange_timestamp", + &dr.details.ok.etime), + TALER_JSON_spec_amount ("merge_amount", + pch->purse_value_after_fees.currency, + &total_deposited), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (pch->keys, + &dr.details.ok.exchange_pub)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID; + break; + } + if (GNUNET_OK != + TALER_exchange_online_purse_merged_verify ( + dr.details.ok.etime, + pch->purse_expiration, + &pch->purse_value_after_fees, + &pch->purse_pub, + &pch->h_contract_terms, + &pch->reserve_pub, + pch->provider_url, + &dr.details.ok.exchange_pub, + &dr.details.ok.exchange_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID; + 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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_PAYMENT_REQUIRED: + /* purse was not (yet) full */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_CONFLICT: + { + struct TALER_PurseMergePublicKeyP merge_pub; + + GNUNET_CRYPTO_eddsa_key_get_public (&pch->merge_priv.eddsa_priv, + &merge_pub.eddsa_pub); + if (GNUNET_OK != + TALER_EXCHANGE_check_purse_merge_conflict_ ( + &pch->merge_sig, + &merge_pub, + &pch->purse_pub, + pch->provider_url, + j)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + } + 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 */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &dr.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange deposit\n", + (unsigned int) response_code, + dr.hr.ec); + GNUNET_break_op (0); + break; + } + pch->cb (pch->cb_cls, + &dr); + TALER_EXCHANGE_account_merge_cancel (pch); +} + + +struct TALER_EXCHANGE_AccountMergeHandle * +TALER_EXCHANGE_account_merge ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const char *reserve_exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const struct TALER_PurseMergePrivateKeyP *merge_priv, + const struct TALER_PrivateContractHashP *h_contract_terms, + uint8_t min_age, + const struct TALER_Amount *purse_value_after_fees, + struct GNUNET_TIME_Timestamp purse_expiration, + struct GNUNET_TIME_Timestamp merge_timestamp, + TALER_EXCHANGE_AccountMergeCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_AccountMergeHandle *pch; + json_t *merge_obj; + CURL *eh; + char arg_str[sizeof (pch->purse_pub) * 2 + 32]; + char *reserve_url; + + pch = GNUNET_new (struct TALER_EXCHANGE_AccountMergeHandle); + pch->merge_priv = *merge_priv; + pch->cb = cb; + pch->cb_cls = cb_cls; + pch->purse_pub = *purse_pub; + pch->h_contract_terms = *h_contract_terms; + pch->purse_expiration = purse_expiration; + pch->purse_value_after_fees = *purse_value_after_fees; + if (NULL == reserve_exchange_url) + pch->provider_url = GNUNET_strdup (url); + else + pch->provider_url = GNUNET_strdup (reserve_exchange_url); + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &pch->reserve_pub.eddsa_pub); + + { + char pub_str[sizeof (*purse_pub) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + purse_pub, + sizeof (*purse_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "purses/%s/merge", + pub_str); + } + reserve_url = TALER_reserve_make_payto (pch->provider_url, + &pch->reserve_pub); + if (NULL == reserve_url) + { + GNUNET_break (0); + GNUNET_free (pch->provider_url); + GNUNET_free (pch); + return NULL; + } + pch->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == pch->url) + { + GNUNET_break (0); + GNUNET_free (reserve_url); + GNUNET_free (pch->provider_url); + GNUNET_free (pch); + return NULL; + } + TALER_wallet_purse_merge_sign (reserve_url, + merge_timestamp, + purse_pub, + merge_priv, + &pch->merge_sig); + { + struct TALER_Amount zero_purse_fee; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (purse_value_after_fees->currency, + &zero_purse_fee)); + TALER_wallet_account_merge_sign (merge_timestamp, + purse_pub, + purse_expiration, + h_contract_terms, + purse_value_after_fees, + &zero_purse_fee, + min_age, + TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, + reserve_priv, + &pch->reserve_sig); + } + merge_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("payto_uri", + reserve_url), + GNUNET_JSON_pack_data_auto ("merge_sig", + &pch->merge_sig), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &pch->reserve_sig), + GNUNET_JSON_pack_timestamp ("merge_timestamp", + merge_timestamp)); + GNUNET_assert (NULL != merge_obj); + GNUNET_free (reserve_url); + eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&pch->ctx, + eh, + merge_obj)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (merge_obj); + GNUNET_free (pch->provider_url); + GNUNET_free (pch->url); + GNUNET_free (pch); + return NULL; + } + json_decref (merge_obj); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "URL for purse merge: `%s'\n", + pch->url); + pch->keys = TALER_EXCHANGE_keys_incref (keys); + pch->job = GNUNET_CURL_job_add2 (ctx, + eh, + pch->ctx.headers, + &handle_purse_merge_finished, + pch); + return pch; +} + + +void +TALER_EXCHANGE_account_merge_cancel ( + struct TALER_EXCHANGE_AccountMergeHandle *pch) +{ + if (NULL != pch->job) + { + GNUNET_CURL_job_cancel (pch->job); + pch->job = NULL; + } + GNUNET_free (pch->url); + GNUNET_free (pch->provider_url); + TALER_curl_easy_post_finished (&pch->ctx); + TALER_EXCHANGE_keys_decref (pch->keys); + GNUNET_free (pch); +} + + +/* end of exchange_api_purse_merge.c */ diff --git a/src/lib/exchange_api_purses_get.c b/src/lib/exchange_api_purses_get.c new file mode 100644 index 000000000..dc22c75ad --- /dev/null +++ b/src/lib/exchange_api_purses_get.c @@ -0,0 +1,302 @@ +/* + This file is part of TALER + Copyright (C) 2022-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_purses_get.c + * @brief Implementation of the /purses/ GET request + * @author Christian Grothoff + */ +#include "platform.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 "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * @brief A Contract Get Handle + */ +struct TALER_EXCHANGE_PurseGetHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_PurseGetCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + +}; + + +/** + * Function called when we're done processing the + * HTTP /purses/$PID GET request. + * + * @param cls the `struct TALER_EXCHANGE_PurseGetHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_purse_get_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_PurseGetHandle *pgh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_PurseGetResponse dr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + pgh->job = NULL; + switch (response_code) + { + case 0: + dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + bool no_merge = false; + bool no_deposit = false; + struct TALER_ExchangePublicKeyP exchange_pub; + struct TALER_ExchangeSignatureP exchange_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &dr.details.ok.merge_timestamp), + &no_merge), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("deposit_timestamp", + &dr.details.ok.deposit_timestamp), + &no_deposit), + TALER_JSON_spec_amount_any ("balance", + &dr.details.ok.balance), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &dr.details.ok.purse_expiration), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (pgh->keys, + &exchange_pub)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE; + break; + } + if (GNUNET_OK != + TALER_exchange_online_purse_status_verify ( + dr.details.ok.merge_timestamp, + dr.details.ok.deposit_timestamp, + &dr.details.ok.balance, + &exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE; + break; + } + pgh->cb (pgh->cb_cls, + &dr); + TALER_EXCHANGE_purse_get_cancel (pgh); + return; + } + case MHD_HTTP_BAD_REQUEST: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* 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 */ + break; + case MHD_HTTP_NOT_FOUND: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Exchange does not know about transaction; + we should pass the reply to the application */ + break; + case MHD_HTTP_GONE: + /* purse expired */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + dr.hr.ec = TALER_JSON_get_error_code (j); + dr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange GET purses\n", + (unsigned int) response_code, + (int) dr.hr.ec); + GNUNET_break_op (0); + break; + } + pgh->cb (pgh->cb_cls, + &dr); + TALER_EXCHANGE_purse_get_cancel (pgh); +} + + +struct TALER_EXCHANGE_PurseGetHandle * +TALER_EXCHANGE_purse_get ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_PurseContractPublicKeyP *purse_pub, + struct GNUNET_TIME_Relative timeout, + bool wait_for_merge, + TALER_EXCHANGE_PurseGetCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_PurseGetHandle *pgh; + CURL *eh; + char arg_str[sizeof (*purse_pub) * 2 + 64]; + unsigned int tms + = (unsigned int) timeout.rel_value_us + / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us; + + pgh = GNUNET_new (struct TALER_EXCHANGE_PurseGetHandle); + pgh->cb = cb; + pgh->cb_cls = cb_cls; + { + char cpub_str[sizeof (*purse_pub) * 2]; + char *end; + char timeout_str[32]; + + end = GNUNET_STRINGS_data_to_string (purse_pub, + sizeof (*purse_pub), + cpub_str, + sizeof (cpub_str)); + *end = '\0'; + GNUNET_snprintf (timeout_str, + sizeof (timeout_str), + "%u", + tms); + if (0 == tms) + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "purses/%s/%s", + cpub_str, + wait_for_merge ? "merge" : "deposit"); + else + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "purses/%s/%s?timeout_ms=%s", + cpub_str, + wait_for_merge ? "merge" : "deposit", + timeout_str); + } + pgh->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == pgh->url) + { + GNUNET_free (pgh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (pgh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (pgh->url); + GNUNET_free (pgh); + return NULL; + } + if (0 != tms) + { + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT_MS, + (long) (tms + 100L))); + } + pgh->job = GNUNET_CURL_job_add (ctx, + eh, + &handle_purse_get_finished, + pgh); + pgh->keys = TALER_EXCHANGE_keys_incref (keys); + return pgh; +} + + +void +TALER_EXCHANGE_purse_get_cancel ( + struct TALER_EXCHANGE_PurseGetHandle *pgh) +{ + if (NULL != pgh->job) + { + GNUNET_CURL_job_cancel (pgh->job); + pgh->job = NULL; + } + GNUNET_free (pgh->url); + TALER_EXCHANGE_keys_decref (pgh->keys); + GNUNET_free (pgh); +} + + +/* end of exchange_api_purses_get.c */ diff --git a/src/lib/exchange_api_recoup.c b/src/lib/exchange_api_recoup.c index 09d99b236..56499f381 100644 --- a/src/lib/exchange_api_recoup.c +++ b/src/lib/exchange_api_recoup.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2017-2021 Taler Systems SA + Copyright (C) 2017-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 @@ -27,6 +27,7 @@ #include <gnunet/gnunet_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" @@ -39,9 +40,9 @@ struct TALER_EXCHANGE_RecoupHandle { /** - * The connection to exchange this request handle will use + * The keys of the exchange this request handle will use */ - struct TALER_EXCHANGE_Handle *exchange; + struct TALER_EXCHANGE_Keys *keys; /** * The url for this request. @@ -60,6 +61,11 @@ struct TALER_EXCHANGE_RecoupHandle struct TALER_EXCHANGE_DenomPublicKey pk; /** + * Our signature requesting the recoup. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** * Handle for the request. */ struct GNUNET_CURL_Job *job; @@ -79,11 +85,6 @@ struct TALER_EXCHANGE_RecoupHandle */ struct TALER_CoinSpendPublicKeyP coin_pub; - /** - * #GNUNET_YES if the coin was refreshed - */ - int was_refreshed; - }; @@ -95,45 +96,30 @@ struct TALER_EXCHANGE_RecoupHandle * @return #GNUNET_OK if the signature is valid and we called the callback; * #GNUNET_SYSERR if not (callback must still be called) */ -static int +static enum GNUNET_GenericReturnValue process_recoup_response (const struct TALER_EXCHANGE_RecoupHandle *ph, const json_t *json) { - int refreshed; - struct TALER_ReservePublicKeyP reserve_pub; - struct TALER_CoinSpendPublicKeyP old_coin_pub; - struct GNUNET_JSON_Specification spec_withdraw[] = { - GNUNET_JSON_spec_boolean ("refreshed", &refreshed), - GNUNET_JSON_spec_fixed_auto ("reserve_pub", &reserve_pub), - GNUNET_JSON_spec_end () + struct TALER_EXCHANGE_RecoupResponse rr = { + .hr.reply = json, + .hr.http_status = MHD_HTTP_OK }; - struct GNUNET_JSON_Specification spec_refresh[] = { - GNUNET_JSON_spec_boolean ("refreshed", &refreshed), - GNUNET_JSON_spec_fixed_auto ("old_coin_pub", &old_coin_pub), + struct GNUNET_JSON_Specification spec_withdraw[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &rr.details.ok.reserve_pub), GNUNET_JSON_spec_end () }; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = json, - .http_status = MHD_HTTP_OK - }; if (GNUNET_OK != GNUNET_JSON_parse (json, - ph->was_refreshed ? spec_refresh : spec_withdraw, + spec_withdraw, NULL, NULL)) { GNUNET_break_op (0); return GNUNET_SYSERR; } - if (ph->was_refreshed != refreshed) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } ph->cb (ph->cb_cls, - &hr, - ph->was_refreshed ? NULL : &reserve_pub, - ph->was_refreshed ? &old_coin_pub : NULL); + &rr); return GNUNET_OK; } @@ -153,16 +139,16 @@ handle_recoup_finished (void *cls, { struct TALER_EXCHANGE_RecoupHandle *ph = cls; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_RecoupResponse rr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; ph->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: if (GNUNET_OK != @@ -170,8 +156,8 @@ handle_recoup_finished (void *cls, j)) { GNUNET_break_op (0); - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - hr.http_status = 0; + rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + rr.hr.http_status = 0; break; } TALER_EXCHANGE_recoup_cancel (ph); @@ -179,201 +165,173 @@ handle_recoup_finished (void *cls, 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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_CONFLICT: { - /* Insufficient funds, proof attached */ - json_t *history; - struct TALER_Amount total; - struct GNUNET_HashCode h_denom_pub; - const struct TALER_EXCHANGE_DenomPublicKey *dki; - enum TALER_ErrorCode ec; - - dki = &ph->pk; - history = json_object_get (j, - "history"); + struct TALER_Amount min_key; + + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); if (GNUNET_OK != - TALER_EXCHANGE_verify_coin_history (dki, - dki->fee_deposit.currency, - &ph->coin_pub, - history, - &h_denom_pub, - &total)) - { - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - } - else + TALER_EXCHANGE_get_min_denomination_ (ph->keys, + &min_key)) { - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); - } - ec = TALER_JSON_get_error_code (j); - switch (ec) - { - case TALER_EC_EXCHANGE_RECOUP_COIN_BALANCE_ZERO: - if (0 > TALER_amount_cmp (&total, - &dki->value)) - { - /* recoup MAY have still been possible */ - /* FIXME: This code may falsely complain, as we do not - know that the smallest denomination offered by the - exchange is here. We should look at the key - structure of ph->exchange, and find the smallest - _currently withdrawable_ denomination and check - if the value remaining would suffice... */// - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - break; - case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: - if (0 == GNUNET_memcmp (&ph->pk.h_key, - &h_denom_pub)) - { - /* invalid proof provided */ - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - /* valid error from exchange */ - break; - default: - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + GNUNET_break (0); + rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + rr.hr.http_status = 0; break; } - ph->cb (ph->cb_cls, - &hr, - NULL, - NULL); - TALER_EXCHANGE_recoup_cancel (ph); - return; + break; } case MHD_HTTP_FORBIDDEN: /* 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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_NOT_FOUND: /* Nothing really to verify, 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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_GONE: /* Kind of normal: the money was already sent to the merchant (it was too late for the refund). */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; default: /* unexpected response code */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange recoup\n", (unsigned int) response_code, - (int) hr.ec); + (int) rr.hr.ec); GNUNET_break (0); break; } ph->cb (ph->cb_cls, - &hr, - NULL, - NULL); + &rr); TALER_EXCHANGE_recoup_cancel (ph); } struct TALER_EXCHANGE_RecoupHandle * -TALER_EXCHANGE_recoup (struct TALER_EXCHANGE_Handle *exchange, - const struct TALER_EXCHANGE_DenomPublicKey *pk, - const struct TALER_DenominationSignature *denom_sig, - const struct TALER_PlanchetSecretsP *ps, - bool was_refreshed, - TALER_EXCHANGE_RecoupResultCallback recoup_cb, - void *recoup_cb_cls) +TALER_EXCHANGE_recoup ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_EXCHANGE_DenomPublicKey *pk, + const struct TALER_DenominationSignature *denom_sig, + const struct TALER_ExchangeWithdrawValues *exchange_vals, + const struct TALER_PlanchetMasterSecretP *ps, + TALER_EXCHANGE_RecoupResultCallback recoup_cb, + void *recoup_cb_cls) { struct TALER_EXCHANGE_RecoupHandle *ph; - struct GNUNET_CURL_Context *ctx; - struct TALER_RecoupRequestPS pr; - struct TALER_CoinSpendSignatureP coin_sig; - struct GNUNET_HashCode h_denom_pub; + struct TALER_DenominationHashP h_denom_pub; json_t *recoup_obj; CURL *eh; char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32]; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; - GNUNET_assert (GNUNET_YES == - TEAH_handle_is_ready (exchange)); - pr.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_RECOUP); - pr.purpose.size = htonl (sizeof (struct TALER_RecoupRequestPS)); - GNUNET_CRYPTO_eddsa_key_get_public (&ps->coin_priv.eddsa_priv, - &pr.coin_pub.eddsa_pub); - GNUNET_CRYPTO_rsa_public_key_hash (pk->key.rsa_public_key, - &h_denom_pub); - pr.h_denom_pub = pk->h_key; - pr.coin_blind = ps->blinding_key; - GNUNET_CRYPTO_eddsa_sign (&ps->coin_priv.eddsa_priv, - &pr, - &coin_sig.eddsa_signature); + ph = GNUNET_new (struct TALER_EXCHANGE_RecoupHandle); + TALER_planchet_setup_coin_priv (ps, + exchange_vals, + &coin_priv); + TALER_planchet_blinding_secret_create (ps, + exchange_vals, + &bks); + GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv, + &ph->coin_pub.eddsa_pub); + TALER_denom_pub_hash (&pk->key, + &h_denom_pub); + TALER_wallet_recoup_sign (&h_denom_pub, + &bks, + &coin_priv, + &ph->coin_sig); recoup_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("denom_pub_hash", &h_denom_pub), - TALER_JSON_pack_denomination_signature ("denom_sig", - denom_sig), + TALER_JSON_pack_denom_sig ("denom_sig", + denom_sig), + TALER_JSON_pack_exchange_withdraw_values ("ewv", + exchange_vals), GNUNET_JSON_pack_data_auto ("coin_sig", - &coin_sig), + &ph->coin_sig), GNUNET_JSON_pack_data_auto ("coin_blind_key_secret", - &ps->blinding_key), - GNUNET_JSON_pack_bool ("refreshed", - was_refreshed)); + &bks)); + switch (denom_sig->unblinded_sig->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + json_decref (recoup_obj); + GNUNET_break (0); + GNUNET_free (ph); + return NULL; + case GNUNET_CRYPTO_BSA_RSA: + break; + case GNUNET_CRYPTO_BSA_CS: + { + union GNUNET_CRYPTO_BlindSessionNonce nonce; + + /* NOTE: this is not elegant, and as per the note in TALER_coin_ev_hash() + it is not strictly clear that the nonce is needed. Best case would be + to find a way to include it more 'naturally' somehow, for example with + the variant union version of bks! */ + TALER_cs_withdraw_nonce_derive (ps, + &nonce.cs_nonce); + GNUNET_assert ( + 0 == + json_object_set_new (recoup_obj, + "cs_nonce", + GNUNET_JSON_from_data_auto ( + &nonce))); + } + } + { char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; char *end; - end = GNUNET_STRINGS_data_to_string (&pr.coin_pub, - sizeof (struct - TALER_CoinSpendPublicKeyP), - pub_str, - sizeof (pub_str)); + end = GNUNET_STRINGS_data_to_string ( + &ph->coin_pub, + sizeof (struct TALER_CoinSpendPublicKeyP), + pub_str, + sizeof (pub_str)); *end = '\0'; GNUNET_snprintf (arg_str, sizeof (arg_str), - "/coins/%s/recoup", + "coins/%s/recoup", pub_str); } - ph = GNUNET_new (struct TALER_EXCHANGE_RecoupHandle); - ph->coin_pub = pr.coin_pub; - ph->exchange = exchange; ph->pk = *pk; - ph->pk.key.rsa_public_key = NULL; /* zero out, as lifetime cannot be warranted */ + memset (&ph->pk.key, + 0, + sizeof (ph->pk.key)); /* zero out, as lifetime cannot be warranted */ ph->cb = recoup_cb; ph->cb_cls = recoup_cb_cls; - ph->url = TEAH_path_to_url (exchange, - arg_str); + ph->url = TALER_url_join (url, + arg_str, + NULL); if (NULL == ph->url) { json_decref (recoup_obj); GNUNET_free (ph); return NULL; } - ph->was_refreshed = was_refreshed; eh = TALER_EXCHANGE_curl_easy_get_ (ph->url); if ( (NULL == eh) || (GNUNET_OK != @@ -393,7 +351,7 @@ TALER_EXCHANGE_recoup (struct TALER_EXCHANGE_Handle *exchange, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "URL for recoup: `%s'\n", ph->url); - ctx = TEAH_handle_to_context (exchange); + ph->keys = TALER_EXCHANGE_keys_incref (keys); ph->job = GNUNET_CURL_job_add2 (ctx, eh, ph->ctx.headers, @@ -413,6 +371,7 @@ TALER_EXCHANGE_recoup_cancel (struct TALER_EXCHANGE_RecoupHandle *ph) } GNUNET_free (ph->url); TALER_curl_easy_post_finished (&ph->ctx); + TALER_EXCHANGE_keys_decref (ph->keys); GNUNET_free (ph); } diff --git a/src/lib/exchange_api_recoup_refresh.c b/src/lib/exchange_api_recoup_refresh.c new file mode 100644 index 000000000..0c2e21cbf --- /dev/null +++ b/src/lib/exchange_api_recoup_refresh.c @@ -0,0 +1,374 @@ +/* + This file is part of TALER + Copyright (C) 2017-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_recoup_refresh.c + * @brief Implementation of the /recoup-refresh request of the exchange's HTTP API + * @author Christian Grothoff + */ +#include "platform.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 "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" + + +/** + * @brief A Recoup Handle + */ +struct TALER_EXCHANGE_RecoupRefreshHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Denomination key of the coin. + */ + struct TALER_EXCHANGE_DenomPublicKey pk; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_RecoupRefreshResultCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Public key of the coin we are trying to get paid back. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature affirming the recoup-refresh operation. + */ + struct TALER_CoinSpendSignatureP coin_sig; + +}; + + +/** + * Parse a recoup-refresh response. If it is valid, call the callback. + * + * @param ph recoup handle + * @param json json reply with the signature + * @return #GNUNET_OK if the signature is valid and we called the callback; + * #GNUNET_SYSERR if not (callback must still be called) + */ +static enum GNUNET_GenericReturnValue +process_recoup_response ( + const struct TALER_EXCHANGE_RecoupRefreshHandle *ph, + const json_t *json) +{ + struct TALER_EXCHANGE_RecoupRefreshResponse rrr = { + .hr.reply = json, + .hr.http_status = MHD_HTTP_OK + }; + struct GNUNET_JSON_Specification spec_refresh[] = { + GNUNET_JSON_spec_fixed_auto ("old_coin_pub", + &rrr.details.ok.old_coin_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec_refresh, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + ph->cb (ph->cb_cls, + &rrr); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /recoup-refresh request. + * + * @param cls the `struct TALER_EXCHANGE_RecoupRefreshHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_recoup_refresh_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_RecoupRefreshHandle *ph = cls; + const json_t *j = response; + struct TALER_EXCHANGE_RecoupRefreshResponse rrr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + ph->job = NULL; + switch (response_code) + { + case 0: + rrr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + process_recoup_response (ph, + j)) + { + GNUNET_break_op (0); + rrr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + rrr.hr.http_status = 0; + break; + } + TALER_EXCHANGE_recoup_refresh_cancel (ph); + 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 */ + rrr.hr.ec = TALER_JSON_get_error_code (j); + rrr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + /* 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 */ + rrr.hr.ec = TALER_JSON_get_error_code (j); + rrr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rrr.hr.ec = TALER_JSON_get_error_code (j); + rrr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + rrr.hr.ec = TALER_JSON_get_error_code (j); + rrr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_GONE: + /* Kind of normal: the money was already sent to the merchant + (it was too late for the refund). */ + rrr.hr.ec = TALER_JSON_get_error_code (j); + rrr.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 */ + rrr.hr.ec = TALER_JSON_get_error_code (j); + rrr.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + rrr.hr.ec = TALER_JSON_get_error_code (j); + rrr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange recoup\n", + (unsigned int) response_code, + (int) rrr.hr.ec); + GNUNET_break (0); + break; + } + ph->cb (ph->cb_cls, + &rrr); + TALER_EXCHANGE_recoup_refresh_cancel (ph); +} + + +struct TALER_EXCHANGE_RecoupRefreshHandle * +TALER_EXCHANGE_recoup_refresh ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_EXCHANGE_DenomPublicKey *pk, + const struct TALER_DenominationSignature *denom_sig, + const struct TALER_ExchangeWithdrawValues *exchange_vals, + const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_PlanchetMasterSecretP *ps, + unsigned int idx, + TALER_EXCHANGE_RecoupRefreshResultCallback recoup_cb, + void *recoup_cb_cls) +{ + struct TALER_EXCHANGE_RecoupRefreshHandle *ph; + struct TALER_DenominationHashP h_denom_pub; + json_t *recoup_obj; + CURL *eh; + char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32]; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; + + GNUNET_assert (NULL != recoup_cb); + ph = GNUNET_new (struct TALER_EXCHANGE_RecoupRefreshHandle); + ph->pk = *pk; + memset (&ph->pk.key, + 0, + sizeof (ph->pk.key)); /* zero out, as lifetime cannot be warranted */ + ph->cb = recoup_cb; + ph->cb_cls = recoup_cb_cls; + TALER_planchet_setup_coin_priv (ps, + exchange_vals, + &coin_priv); + TALER_planchet_blinding_secret_create (ps, + exchange_vals, + &bks); + GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv, + &ph->coin_pub.eddsa_pub); + TALER_denom_pub_hash (&pk->key, + &h_denom_pub); + TALER_wallet_recoup_refresh_sign (&h_denom_pub, + &bks, + &coin_priv, + &ph->coin_sig); + recoup_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &h_denom_pub), + TALER_JSON_pack_denom_sig ("denom_sig", + denom_sig), + TALER_JSON_pack_exchange_withdraw_values ("ewv", + exchange_vals), + GNUNET_JSON_pack_data_auto ("coin_sig", + &ph->coin_sig), + GNUNET_JSON_pack_data_auto ("coin_blind_key_secret", + &bks)); + + switch (denom_sig->unblinded_sig->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + json_decref (recoup_obj); + GNUNET_break (0); + GNUNET_free (ph); + return NULL; + case GNUNET_CRYPTO_BSA_RSA: + break; + case GNUNET_CRYPTO_BSA_CS: + { + union GNUNET_CRYPTO_BlindSessionNonce nonce; + + /* NOTE: this is not elegant, and as per the note in TALER_coin_ev_hash() + it is not strictly clear that the nonce is needed. Best case would be + to find a way to include it more 'naturally' somehow, for example with + the variant union version of bks! */ + TALER_cs_refresh_nonce_derive (rms, + idx, + &nonce.cs_nonce); + GNUNET_assert ( + 0 == + json_object_set_new (recoup_obj, + "cs_nonce", + GNUNET_JSON_from_data_auto ( + &nonce))); + } + break; + } + + { + char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &ph->coin_pub, + sizeof (struct TALER_CoinSpendPublicKeyP), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "coins/%s/recoup-refresh", + pub_str); + } + + ph->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == ph->url) + { + json_decref (recoup_obj); + GNUNET_free (ph); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (ph->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&ph->ctx, + eh, + recoup_obj)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (recoup_obj); + GNUNET_free (ph->url); + GNUNET_free (ph); + return NULL; + } + json_decref (recoup_obj); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "URL for recoup-refresh: `%s'\n", + ph->url); + ph->keys = TALER_EXCHANGE_keys_incref (keys); + ph->job = GNUNET_CURL_job_add2 (ctx, + eh, + ph->ctx.headers, + &handle_recoup_refresh_finished, + ph); + return ph; +} + + +void +TALER_EXCHANGE_recoup_refresh_cancel ( + struct TALER_EXCHANGE_RecoupRefreshHandle *ph) +{ + if (NULL != ph->job) + { + GNUNET_CURL_job_cancel (ph->job); + ph->job = NULL; + } + GNUNET_free (ph->url); + TALER_curl_easy_post_finished (&ph->ctx); + TALER_EXCHANGE_keys_decref (ph->keys); + GNUNET_free (ph); +} + + +/* end of exchange_api_recoup_refresh.c */ diff --git a/src/lib/exchange_api_refresh_common.c b/src/lib/exchange_api_refresh_common.c index 00d01e4e5..4369367e4 100644 --- a/src/lib/exchange_api_refresh_common.c +++ b/src/lib/exchange_api_refresh_common.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2020 Taler Systems SA + Copyright (C) 2015-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 @@ -23,599 +23,234 @@ #include "exchange_api_refresh_common.h" -/** - * Free all information associated with a melted coin session. - * - * @param mc melted coin to release, the pointer itself is NOT - * freed (as it is typically not allocated by itself) - */ -static void -free_melted_coin (struct MeltedCoin *mc) -{ - if (NULL != mc->pub_key.rsa_public_key) - GNUNET_CRYPTO_rsa_public_key_free (mc->pub_key.rsa_public_key); - if (NULL != mc->sig.rsa_signature) - GNUNET_CRYPTO_rsa_signature_free (mc->sig.rsa_signature); -} - - -/** - * Free all information associated with a melting session. Note - * that we allow the melting session to be only partially initialized, - * as we use this function also when freeing melt data that was not - * fully initialized (i.e. due to failures in #TALER_EXCHANGE_deserialize_melt_data_()). - * - * @param md melting data to release, the pointer itself is NOT - * freed (as it is typically not allocated by itself) - */ void TALER_EXCHANGE_free_melt_data_ (struct MeltData *md) { - free_melted_coin (&md->melted_coin); - if (NULL != md->fresh_pks) - { - for (unsigned int i = 0; i<md->num_fresh_coins; i++) - if (NULL != md->fresh_pks[i].rsa_public_key) - GNUNET_CRYPTO_rsa_public_key_free (md->fresh_pks[i].rsa_public_key); - GNUNET_free (md->fresh_pks); - } - - for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) - GNUNET_free (md->fresh_coins[i]); - /* Finally, clean up a bit... */ - GNUNET_CRYPTO_zero_keys (md, - sizeof (struct MeltData)); -} - - -/** - * Serialize information about a coin we are melting. - * - * @param mc information to serialize - * @param buf buffer to write data in, NULL to just compute - * required size - * @param off offeset at @a buf to use - * @return number of bytes written to @a buf at @a off, or if - * @a buf is NULL, number of bytes required; 0 on error - */ -static size_t -serialize_melted_coin (const struct MeltedCoin *mc, - char *buf, - size_t off) -{ - struct MeltedCoinP mcp; - void *pbuf; - size_t pbuf_size; - void *sbuf; - size_t sbuf_size; - - sbuf_size = GNUNET_CRYPTO_rsa_signature_encode (mc->sig.rsa_signature, - &sbuf); - pbuf_size = GNUNET_CRYPTO_rsa_public_key_encode (mc->pub_key.rsa_public_key, - &pbuf); - if (NULL == buf) - { - GNUNET_free (sbuf); - GNUNET_free (pbuf); - return sizeof (struct MeltedCoinP) + sbuf_size + pbuf_size; - } - if ( (sbuf_size > UINT16_MAX) || - (pbuf_size > UINT16_MAX) ) - { - GNUNET_break (0); - return 0; - } - mcp.coin_priv = mc->coin_priv; - TALER_amount_hton (&mcp.melt_amount_with_fee, - &mc->melt_amount_with_fee); - TALER_amount_hton (&mcp.fee_melt, - &mc->fee_melt); - TALER_amount_hton (&mcp.original_value, - &mc->original_value); - for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) - mcp.transfer_priv[i] = mc->transfer_priv[i]; - mcp.expire_deposit = GNUNET_TIME_absolute_hton (mc->expire_deposit); - mcp.pbuf_size = htons ((uint16_t) pbuf_size); - mcp.sbuf_size = htons ((uint16_t) sbuf_size); - memcpy (&buf[off], - &mcp, - sizeof (struct MeltedCoinP)); - memcpy (&buf[off + sizeof (struct MeltedCoinP)], - pbuf, - pbuf_size); - memcpy (&buf[off + sizeof (struct MeltedCoinP) + pbuf_size], - sbuf, - sbuf_size); - GNUNET_free (sbuf); - GNUNET_free (pbuf); - return sizeof (struct MeltedCoinP) + sbuf_size + pbuf_size; -} - - -/** - * Deserialize information about a coin we are melting. - * - * @param[out] mc information to deserialize - * @param buf buffer to read data from - * @param size number of bytes available at @a buf to use - * @param[out] ok set to #GNUNET_NO to report errors - * @return number of bytes read from @a buf, 0 on error - */ -static size_t -deserialize_melted_coin (struct MeltedCoin *mc, - const char *buf, - size_t size, - int *ok) -{ - struct MeltedCoinP mcp; - size_t pbuf_size; - size_t sbuf_size; - size_t off; - - if (size < sizeof (struct MeltedCoinP)) - { - GNUNET_break (0); - *ok = GNUNET_NO; - return 0; - } - memcpy (&mcp, - buf, - sizeof (struct MeltedCoinP)); - pbuf_size = ntohs (mcp.pbuf_size); - sbuf_size = ntohs (mcp.sbuf_size); - if (size < sizeof (struct MeltedCoinP) + pbuf_size + sbuf_size) - { - GNUNET_break (0); - *ok = GNUNET_NO; - return 0; - } - off = sizeof (struct MeltedCoinP); - mc->pub_key.rsa_public_key - = GNUNET_CRYPTO_rsa_public_key_decode (&buf[off], - pbuf_size); - off += pbuf_size; - mc->sig.rsa_signature - = GNUNET_CRYPTO_rsa_signature_decode (&buf[off], - sbuf_size); - off += sbuf_size; - if ( (NULL == mc->pub_key.rsa_public_key) || - (NULL == mc->sig.rsa_signature) ) + for (unsigned int i = 0; i < TALER_CNC_KAPPA; i++) { - GNUNET_break (0); - *ok = GNUNET_NO; - return 0; - } - - mc->coin_priv = mcp.coin_priv; - TALER_amount_ntoh (&mc->melt_amount_with_fee, - &mcp.melt_amount_with_fee); - TALER_amount_ntoh (&mc->fee_melt, - &mcp.fee_melt); - TALER_amount_ntoh (&mc->original_value, - &mcp.original_value); - for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) - mc->transfer_priv[i] = mcp.transfer_priv[i]; - mc->expire_deposit = GNUNET_TIME_absolute_ntoh (mcp.expire_deposit); - return off; -} - + struct TALER_RefreshCoinData *rcds = md->rcd[i]; -/** - * Serialize information about a denomination key. - * - * @param dk information to serialize - * @param buf buffer to write data in, NULL to just compute - * required size - * @param off offset at @a buf to use - * @return number of bytes written to @a buf at @a off (in addition to @a off itself), or if - * @a buf is NULL, number of bytes required, excluding @a off - */ -static size_t -serialize_denomination_key (const struct TALER_DenominationPublicKey *dk, - char *buf, - size_t off) -{ - void *pbuf; - size_t pbuf_size; - uint32_t be; - - pbuf_size = GNUNET_CRYPTO_rsa_public_key_encode (dk->rsa_public_key, - &pbuf); - if (NULL == buf) - { - GNUNET_free (pbuf); - return pbuf_size + sizeof (uint32_t); + if (NULL == rcds) + continue; + for (unsigned int j = 0; j < md->num_fresh_coins; j++) + TALER_blinded_planchet_free (&rcds[j].blinded_planchet); + GNUNET_free (rcds); } - be = htonl ((uint32_t) pbuf_size); - memcpy (&buf[off], - &be, - sizeof (uint32_t)); - memcpy (&buf[off + sizeof (uint32_t)], - pbuf, - pbuf_size); - GNUNET_free (pbuf); - return pbuf_size + sizeof (uint32_t); -} - - -/** - * Deserialize information about a denomination key. - * - * @param[out] dk information to deserialize - * @param buf buffer to read data from - * @param size number of bytes available at @a buf to use - * @param[out] ok set to #GNUNET_NO to report errors - * @return number of bytes read from @a buf, 0 on error - */ -static size_t -deserialize_denomination_key (struct TALER_DenominationPublicKey *dk, - const char *buf, - size_t size, - int *ok) -{ - size_t pbuf_size; - uint32_t be; - - if (size < sizeof (uint32_t)) - { - GNUNET_break (0); - *ok = GNUNET_NO; - return 0; - } - memcpy (&be, - buf, - sizeof (uint32_t)); - pbuf_size = ntohl (be); - if ( (size < sizeof (uint32_t) + pbuf_size) || - (sizeof (uint32_t) + pbuf_size < pbuf_size) ) + TALER_denom_pub_free (&md->melted_coin.pub_key); + TALER_denom_sig_free (&md->melted_coin.sig); + if (NULL != md->fcds) { - GNUNET_break (0); - *ok = GNUNET_NO; - return 0; - } - dk->rsa_public_key - = GNUNET_CRYPTO_rsa_public_key_decode (&buf[sizeof (uint32_t)], - pbuf_size); - if (NULL == dk->rsa_public_key) - { - GNUNET_break (0); - *ok = GNUNET_NO; - return 0; - } - return sizeof (uint32_t) + pbuf_size; -} - - -/** - * Serialize information about a fresh coin we are generating. - * - * @param fc information to serialize - * @param buf buffer to write data in, NULL to just compute - * required size - * @param off offeset at @a buf to use - * @return number of bytes written to @a buf at @a off, or if - * @a buf is NULL, number of bytes required - */ -static size_t -serialize_fresh_coin (const struct TALER_PlanchetSecretsP *fc, - char *buf, - size_t off) -{ - if (NULL != buf) - memcpy (&buf[off], - fc, - sizeof (struct TALER_PlanchetSecretsP)); - return sizeof (struct TALER_PlanchetSecretsP); -} - - -/** - * Deserialize information about a fresh coin we are generating. - * - * @param[out] fc information to deserialize - * @param buf buffer to read data from - * @param size number of bytes available at @a buf to use - * @param[out] ok set to #GNUNET_NO to report errors - * @return number of bytes read from @a buf, 0 on error - */ -static size_t -deserialize_fresh_coin (struct TALER_PlanchetSecretsP *fc, - const char *buf, - size_t size, - int *ok) -{ - if (size < sizeof (struct TALER_PlanchetSecretsP)) - { - GNUNET_break (0); - *ok = GNUNET_NO; - return 0; - } - memcpy (fc, - buf, - sizeof (struct TALER_PlanchetSecretsP)); - return sizeof (struct TALER_PlanchetSecretsP); -} - - -/** - * Serialize melt data. - * - * @param md data to serialize - * @param[out] res_size size of buffer returned - * @return serialized melt data - */ -static char * -serialize_melt_data (const struct MeltData *md, - size_t *res_size) -{ - size_t size; - size_t asize; - char *buf; - - size = 0; - asize = (size_t) -1; /* make the compiler happy */ - buf = NULL; - /* we do 2 iterations, #1 to determine total size, #2 to - actually construct the buffer */ - do { - if (0 == size) - { - size = sizeof (struct MeltDataP); - } - else + for (unsigned int j = 0; j<md->num_fresh_coins; j++) { - struct MeltDataP *mdp; + struct FreshCoinData *fcd = &md->fcds[j]; - buf = GNUNET_malloc (size); - asize = size; /* just for invariant check later */ - size = sizeof (struct MeltDataP); - mdp = (struct MeltDataP *) buf; - mdp->rc = md->rc; - mdp->num_fresh_coins = htons (md->num_fresh_coins); + TALER_denom_pub_free (&fcd->fresh_pk); + for (size_t i = 0; i < TALER_CNC_KAPPA; i++) + { + TALER_age_commitment_proof_free (fcd->age_commitment_proofs[i]); + GNUNET_free (fcd->age_commitment_proofs[i]); + } } - size += serialize_melted_coin (&md->melted_coin, - buf, - size); - for (unsigned int i = 0; i<md->num_fresh_coins; i++) - size += serialize_denomination_key (&md->fresh_pks[i], - buf, - size); - for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) - for (unsigned int j = 0; j<md->num_fresh_coins; j++) - size += serialize_fresh_coin (&md->fresh_coins[i][j], - buf, - size); - } while (NULL == buf); - GNUNET_assert (size == asize); - *res_size = size; - return buf; -} - - -/** - * Deserialize melt data. - * - * @param buf serialized data - * @param buf_size size of @a buf - * @return deserialized melt data, NULL on error - */ -struct MeltData * -TALER_EXCHANGE_deserialize_melt_data_ (const char *buf, - size_t buf_size) -{ - struct MeltData *md; - struct MeltDataP mdp; - size_t off; - int ok; - - if (buf_size < sizeof (struct MeltDataP)) - return NULL; - memcpy (&mdp, - buf, - sizeof (struct MeltDataP)); - md = GNUNET_new (struct MeltData); - md->rc = mdp.rc; - md->num_fresh_coins = ntohs (mdp.num_fresh_coins); - md->fresh_pks = GNUNET_new_array (md->num_fresh_coins, - struct TALER_DenominationPublicKey); - for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) - md->fresh_coins[i] = GNUNET_new_array (md->num_fresh_coins, - struct TALER_PlanchetSecretsP); - off = sizeof (struct MeltDataP); - ok = GNUNET_YES; - off += deserialize_melted_coin (&md->melted_coin, - &buf[off], - buf_size - off, - &ok); - for (unsigned int i = 0; (i<md->num_fresh_coins) && (GNUNET_YES == ok); i++) - off += deserialize_denomination_key (&md->fresh_pks[i], - &buf[off], - buf_size - off, - &ok); - - for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) - for (unsigned int j = 0; (j<md->num_fresh_coins) && (GNUNET_YES == ok); j++) - off += deserialize_fresh_coin (&md->fresh_coins[i][j], - &buf[off], - buf_size - off, - &ok); - if (off != buf_size) - { - GNUNET_break (0); - ok = GNUNET_NO; + GNUNET_free (md->fcds); } - if (GNUNET_YES != ok) - { - TALER_EXCHANGE_free_melt_data_ (md); - GNUNET_free (md); - return NULL; - } - return md; + /* Finally, clean up a bit... */ + GNUNET_CRYPTO_zero_keys (md, + sizeof (struct MeltData)); } -/** - * Melt (partially spent) coins to obtain fresh coins that are - * unlinkable to the original coin(s). Note that melting more - * than one coin in a single request will make those coins linkable, - * so the safest operation only melts one coin at a time. - * - * This API is typically used by a wallet. Note that to ensure that - * no money is lost in case of hardware failures, this operation does - * not actually initiate the request. Instead, it generates a buffer - * which the caller must store before proceeding with the actual call - * to #TALER_EXCHANGE_melt() that will generate the request. - * - * This function does verify that the given request data is internally - * consistent. However, the @a melts_sigs are NOT verified. - * - * Aside from some non-trivial cryptographic operations that might - * take a bit of CPU time to complete, this function returns - * its result immediately and does not start any asynchronous - * processing. This function is also thread-safe. - * - * @param melt_priv private key of the coin to melt - * @param melt_amount amount specifying how much - * the coin will contribute to the melt (including fee) - * @param melt_sig signature affirming the - * validity of the public keys corresponding to the - * @a melt_priv private key - * @param melt_pk denomination key information - * record corresponding to the @a melt_sig - * validity of the keys - * @param fresh_pks_len length of the @a pks array - * @param fresh_pks array of @a pks_len denominations of fresh coins to create - * @param[out] res_size set to the size of the return value, or 0 on error - * @return NULL - * if the inputs are invalid (i.e. denomination key not with this exchange). - * Otherwise, pointer to a buffer of @a res_size to store persistently - * before proceeding to #TALER_EXCHANGE_melt(). - * Non-null results should be freed using GNUNET_free(). - */ -char * -TALER_EXCHANGE_refresh_prepare ( - const struct TALER_CoinSpendPrivateKeyP *melt_priv, - const struct TALER_Amount *melt_amount, - const struct TALER_DenominationSignature *melt_sig, - const struct TALER_EXCHANGE_DenomPublicKey *melt_pk, - unsigned int fresh_pks_len, - const struct TALER_EXCHANGE_DenomPublicKey *fresh_pks, - size_t *res_size) +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_get_melt_data_ ( + const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_EXCHANGE_RefreshData *rd, + const struct TALER_ExchangeWithdrawValues *alg_values, + struct MeltData *md) { - struct MeltData md; - char *buf; struct TALER_Amount total; struct TALER_CoinSpendPublicKeyP coin_pub; - struct TALER_TransferSecretP trans_sec[TALER_CNC_KAPPA]; - struct TALER_RefreshCommitmentEntry rce[TALER_CNC_KAPPA]; + union GNUNET_CRYPTO_BlindSessionNonce nonces[rd->fresh_pks_len]; + bool uses_cs = false; - GNUNET_CRYPTO_eddsa_key_get_public (&melt_priv->eddsa_priv, + GNUNET_CRYPTO_eddsa_key_get_public (&rd->melt_priv.eddsa_priv, &coin_pub.eddsa_pub); /* build up melt data structure */ - memset (&md, 0, sizeof (md)); - md.num_fresh_coins = fresh_pks_len; - md.melted_coin.coin_priv = *melt_priv; - md.melted_coin.melt_amount_with_fee = *melt_amount; - md.melted_coin.fee_melt = melt_pk->fee_refresh; - md.melted_coin.original_value = melt_pk->value; - md.melted_coin.expire_deposit - = melt_pk->expire_deposit; + memset (md, + 0, + sizeof (*md)); + md->num_fresh_coins = rd->fresh_pks_len; + md->melted_coin.coin_priv = rd->melt_priv; + md->melted_coin.melt_amount_with_fee = rd->melt_amount; + md->melted_coin.fee_melt = rd->melt_pk.fees.refresh; + md->melted_coin.original_value = rd->melt_pk.value; + md->melted_coin.expire_deposit = rd->melt_pk.expire_deposit; + md->melted_coin.age_commitment_proof = rd->melt_age_commitment_proof; + md->melted_coin.h_age_commitment = rd->melt_h_age_commitment; + GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (melt_amount->currency, + TALER_amount_set_zero (rd->melt_amount.currency, &total)); - md.melted_coin.pub_key.rsa_public_key - = GNUNET_CRYPTO_rsa_public_key_dup (melt_pk->key.rsa_public_key); - md.melted_coin.sig.rsa_signature - = GNUNET_CRYPTO_rsa_signature_dup (melt_sig->rsa_signature); - md.fresh_pks = GNUNET_new_array (fresh_pks_len, - struct TALER_DenominationPublicKey); - for (unsigned int i = 0; i<fresh_pks_len; i++) + TALER_denom_pub_copy (&md->melted_coin.pub_key, + &rd->melt_pk.key); + TALER_denom_sig_copy (&md->melted_coin.sig, + &rd->melt_sig); + md->fcds = GNUNET_new_array (md->num_fresh_coins, + struct FreshCoinData); + for (unsigned int j = 0; j<rd->fresh_pks_len; j++) { - md.fresh_pks[i].rsa_public_key - = GNUNET_CRYPTO_rsa_public_key_dup (fresh_pks[i].key.rsa_public_key); + struct FreshCoinData *fcd = &md->fcds[j]; + + TALER_denom_pub_copy (&fcd->fresh_pk, + &rd->fresh_pks[j].key); + GNUNET_assert (NULL != fcd->fresh_pk.bsign_pub_key); + if (alg_values[j].blinding_inputs->cipher != + fcd->fresh_pk.bsign_pub_key->cipher) + { + GNUNET_break (0); + TALER_EXCHANGE_free_melt_data_ (md); + return GNUNET_SYSERR; + } + switch (fcd->fresh_pk.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_break (0); + TALER_EXCHANGE_free_melt_data_ (md); + return GNUNET_SYSERR; + case GNUNET_CRYPTO_BSA_RSA: + break; + case GNUNET_CRYPTO_BSA_CS: + uses_cs = true; + TALER_cs_refresh_nonce_derive (rms, + j, + &nonces[j].cs_nonce); + break; + } if ( (0 > TALER_amount_add (&total, &total, - &fresh_pks[i].value)) || + &rd->fresh_pks[j].value)) || (0 > TALER_amount_add (&total, &total, - &fresh_pks[i].fee_withdraw)) ) + &rd->fresh_pks[j].fees.withdraw)) ) { GNUNET_break (0); - TALER_EXCHANGE_free_melt_data_ (&md); - return NULL; + TALER_EXCHANGE_free_melt_data_ (md); + return GNUNET_SYSERR; } } + /* verify that melt_amount is above total cost */ if (1 == TALER_amount_cmp (&total, - melt_amount) ) + &rd->melt_amount) ) { /* Eh, this operation is more expensive than the @a melt_amount. This is not OK. */ GNUNET_break (0); - TALER_EXCHANGE_free_melt_data_ (&md); - return NULL; + TALER_EXCHANGE_free_melt_data_ (md); + return GNUNET_SYSERR; } /* build up coins */ for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) { - GNUNET_CRYPTO_ecdhe_key_create ( - &md.melted_coin.transfer_priv[i].ecdhe_priv); + struct TALER_TransferSecretP trans_sec; + + TALER_planchet_secret_to_transfer_priv ( + rms, + &rd->melt_priv, + i, + &md->transfer_priv[i]); + GNUNET_CRYPTO_ecdhe_key_get_public ( - &md.melted_coin.transfer_priv[i].ecdhe_priv, - &rce[i].transfer_pub.ecdhe_pub); - TALER_link_derive_transfer_secret (melt_priv, - &md.melted_coin.transfer_priv[i], - &trans_sec[i]); - md.fresh_coins[i] = GNUNET_new_array (fresh_pks_len, - struct TALER_PlanchetSecretsP); - rce[i].new_coins = GNUNET_new_array (fresh_pks_len, - struct TALER_RefreshCoinData); - for (unsigned int j = 0; j<fresh_pks_len; j++) + &md->transfer_priv[i].ecdhe_priv, + &md->transfer_pub[i].ecdhe_pub); + + TALER_link_derive_transfer_secret (&rd->melt_priv, + &md->transfer_priv[i], + &trans_sec); + + md->rcd[i] = GNUNET_new_array (rd->fresh_pks_len, + struct TALER_RefreshCoinData); + + for (unsigned int j = 0; j<rd->fresh_pks_len; j++) { - struct TALER_PlanchetSecretsP *fc = &md.fresh_coins[i][j]; - struct TALER_RefreshCoinData *rcd = &rce[i].new_coins[j]; + struct FreshCoinData *fcd = &md->fcds[j]; + struct TALER_CoinSpendPrivateKeyP *coin_priv = &fcd->coin_priv; + struct TALER_PlanchetMasterSecretP *ps = &fcd->ps[i]; + struct TALER_RefreshCoinData *rcd = &md->rcd[i][j]; + union GNUNET_CRYPTO_BlindingSecretP *bks = &fcd->bks[i]; struct TALER_PlanchetDetail pd; - struct GNUNET_HashCode c_hash; + struct TALER_CoinPubHashP c_hash; + struct TALER_AgeCommitmentHash ach; + struct TALER_AgeCommitmentHash *pah = NULL; + + TALER_transfer_secret_to_planchet_secret (&trans_sec, + j, + ps); + + TALER_planchet_setup_coin_priv (ps, + &alg_values[j], + coin_priv); + + TALER_planchet_blinding_secret_create (ps, + &alg_values[j], + bks); + + if (NULL != rd->melt_age_commitment_proof) + { + fcd->age_commitment_proofs[i] = GNUNET_new (struct + TALER_AgeCommitmentProof); + + GNUNET_assert (GNUNET_OK == + TALER_age_commitment_derive ( + md->melted_coin.age_commitment_proof, + &trans_sec.key, + fcd->age_commitment_proofs[i])); + + TALER_age_commitment_hash ( + &fcd->age_commitment_proofs[i]->commitment, + &ach); + pah = &ach; + } - TALER_planchet_setup_refresh (&trans_sec[i], - j, - fc); if (GNUNET_OK != - TALER_planchet_prepare (&md.fresh_pks[j], - fc, + TALER_planchet_prepare (&fcd->fresh_pk, + &alg_values[j], + bks, + &nonces[j], + coin_priv, + pah, &c_hash, &pd)) { GNUNET_break_op (0); - TALER_EXCHANGE_free_melt_data_ (&md); - return NULL; + TALER_EXCHANGE_free_melt_data_ (md); + return GNUNET_SYSERR; } - rcd->dk = &md.fresh_pks[j]; - rcd->coin_ev = pd.coin_ev; - rcd->coin_ev_size = pd.coin_ev_size; + rcd->blinded_planchet = pd.blinded_planchet; + rcd->dk = &fcd->fresh_pk; } } - /* Compute refresh commitment */ - TALER_refresh_get_commitment (&md.rc, - TALER_CNC_KAPPA, - fresh_pks_len, - rce, - &coin_pub, - melt_amount); - /* finally, serialize everything */ - buf = serialize_melt_data (&md, - res_size); - for (unsigned int i = 0; i < TALER_CNC_KAPPA; i++) + /* Finally, compute refresh commitment */ { - for (unsigned int j = 0; j < fresh_pks_len; j++) - GNUNET_free (rce[i].new_coins[j].coin_ev); - GNUNET_free (rce[i].new_coins); + struct TALER_RefreshCommitmentEntry rce[TALER_CNC_KAPPA]; + + for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++) + { + rce[i].transfer_pub = md->transfer_pub[i]; + rce[i].new_coins = md->rcd[i]; + } + TALER_refresh_get_commitment (&md->rc, + TALER_CNC_KAPPA, + uses_cs + ? rms + : NULL, + rd->fresh_pks_len, + rce, + &coin_pub, + &rd->melt_amount); } - TALER_EXCHANGE_free_melt_data_ (&md); - return buf; + return GNUNET_OK; } diff --git a/src/lib/exchange_api_refresh_common.h b/src/lib/exchange_api_refresh_common.h index 9f2715a1f..f596e1e90 100644 --- a/src/lib/exchange_api_refresh_common.h +++ b/src/lib/exchange_api_refresh_common.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2020 Taler Systems SA + Copyright (C) 2015-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 @@ -27,15 +27,10 @@ #include "taler_signatures.h" -/* structures for committing refresh data to disk before doing the - network interaction(s) */ - -GNUNET_NETWORK_STRUCT_BEGIN - /** - * Header of serialized information about a coin we are melting. + * Information about a coin we are melting. */ -struct MeltedCoinP +struct MeltedCoin { /** * Private key of the coin. @@ -45,103 +40,106 @@ struct MeltedCoinP /** * Amount this coin contributes to the melt, including fee. */ - struct TALER_AmountNBO melt_amount_with_fee; + struct TALER_Amount melt_amount_with_fee; /** - * The applicable fee for withdrawing a coin of this denomination + * The applicable fee for melting a coin of this denomination */ - struct TALER_AmountNBO fee_melt; + struct TALER_Amount fee_melt; /** * The original value of the coin. */ - struct TALER_AmountNBO original_value; + struct TALER_Amount original_value; /** - * Transfer private keys for each cut-and-choose dimension. + * The original age commitment, its proof and its hash. MUST be NULL if no + * age commitment was set. */ - struct TALER_TransferPrivateKeyP transfer_priv[TALER_CNC_KAPPA]; + const struct TALER_AgeCommitmentProof *age_commitment_proof; + const struct TALER_AgeCommitmentHash *h_age_commitment; /** * Timestamp indicating when coins of this denomination become invalid. */ - struct GNUNET_TIME_AbsoluteNBO expire_deposit; + struct GNUNET_TIME_Timestamp expire_deposit; /** - * Size of the encoded public key that follows. + * Denomination key of the original coin. */ - uint16_t pbuf_size; + struct TALER_DenominationPublicKey pub_key; /** - * Size of the encoded signature that follows. + * Exchange's signature over the coin. */ - uint16_t sbuf_size; + struct TALER_DenominationSignature sig; - /* Followed by serializations of: - 1) struct TALER_DenominationPublicKey pub_key; - 2) struct TALER_DenominationSignature sig; - */ }; /** - * Header of serialized data about a melt operation, suitable for - * persisting it on disk. + * Data we keep for each fresh coin created in the + * melt process. */ -struct MeltDataP +struct FreshCoinData { - /** - * Hash over the melting session. + * Denomination public key of the coin. */ - struct TALER_RefreshCommitmentP rc; + struct TALER_DenominationPublicKey fresh_pk; /** - * Number of coins we are melting, in NBO + * Array of planchet secrets for the coins, depending + * on the cut-and-choose. */ - uint16_t num_melted_coins GNUNET_PACKED; + struct TALER_PlanchetMasterSecretP ps[TALER_CNC_KAPPA]; /** - * Number of coins we are creating, in NBO + * Private key of the coin. */ - uint16_t num_fresh_coins GNUNET_PACKED; + struct TALER_CoinSpendPrivateKeyP coin_priv; - /* Followed by serializations of: - 1) struct MeltedCoinP melted_coins[num_melted_coins]; - 2) struct TALER_EXCHANGE_DenomPublicKey fresh_pks[num_fresh_coins]; - 3) TALER_CNC_KAPPA times: - 3a) struct TALER_PlanchetSecretsP fresh_coins[num_fresh_coins]; - */ -}; + /** + * Arrays of age commitments and proofs to be created, one for each + * cut-and-choose dimension. NULL if age restriction is not applicable. + */ + struct TALER_AgeCommitmentProof *age_commitment_proofs[TALER_CNC_KAPPA]; + /** + * Blinding key secrets for the coins, depending on the + * cut-and-choose. + */ + union GNUNET_CRYPTO_BlindingSecretP bks[TALER_CNC_KAPPA]; -GNUNET_NETWORK_STRUCT_END +}; /** - * Information about a coin we are melting. + * Melt data in non-serialized format for convenient processing. */ -struct MeltedCoin +struct MeltData { + /** - * Private key of the coin. + * Hash over the committed data during refresh operation. */ - struct TALER_CoinSpendPrivateKeyP coin_priv; + struct TALER_RefreshCommitmentP rc; /** - * Amount this coin contributes to the melt, including fee. + * Information about the melted coin. */ - struct TALER_Amount melt_amount_with_fee; + struct MeltedCoin melted_coin; /** - * The applicable fee for melting a coin of this denomination + * Array of length @e num_fresh_coins with information + * about each fresh coin. */ - struct TALER_Amount fee_melt; + struct FreshCoinData *fcds; /** - * The original value of the coin. + * Transfer secrets, one per cut and choose. */ - struct TALER_Amount original_value; + struct TALER_TransferSecretP trans_sec[TALER_CNC_KAPPA]; /** * Transfer private keys for each cut-and-choose dimension. @@ -149,77 +147,52 @@ struct MeltedCoin struct TALER_TransferPrivateKeyP transfer_priv[TALER_CNC_KAPPA]; /** - * Timestamp indicating when coins of this denomination become invalid. + * Transfer public key of this commitment. */ - struct GNUNET_TIME_Absolute expire_deposit; + struct TALER_TransferPublicKeyP transfer_pub[TALER_CNC_KAPPA]; /** - * Denomination key of the original coin. + * Transfer secrets, one per cut and choose. */ - struct TALER_DenominationPublicKey pub_key; + struct TALER_RefreshCommitmentEntry rce[TALER_CNC_KAPPA]; /** - * Exchange's signature over the coin. + * Blinded planchets and denominations of the fresh coins, depending on the cut-and-choose. Array of length + * @e num_fresh_coins. */ - struct TALER_DenominationSignature sig; - -}; - - -/** - * Melt data in non-serialized format for convenient processing. - */ -struct MeltData -{ - - /** - * Hash over the committed data during refresh operation. - */ - struct TALER_RefreshCommitmentP rc; + struct TALER_RefreshCoinData *rcd[TALER_CNC_KAPPA]; /** * Number of coins we are creating */ uint16_t num_fresh_coins; - /** - * Information about the melted coin. - */ - struct MeltedCoin melted_coin; - - /** - * Array of @e num_fresh_coins denomination keys for the coins to be - * freshly exchangeed. - */ - struct TALER_DenominationPublicKey *fresh_pks; - - /** - * Arrays of @e num_fresh_coins with information about the fresh - * coins to be created, for each cut-and-choose dimension. - */ - struct TALER_PlanchetSecretsP *fresh_coins[TALER_CNC_KAPPA]; }; /** - * Deserialize melt data. + * Compute the melt data from the refresh data and secret. * - * @param buf serialized data - * @param buf_size size of @a buf - * @return deserialized melt data, NULL on error + * @param rms secret internals of the refresh-reveal operation + * @param rd refresh data with the characteristics of the operation + * @param alg_values contributions from the exchange into the melt + * @param[out] md where to write the derived melt data */ -struct MeltData * -TALER_EXCHANGE_deserialize_melt_data_ (const char *buf, - size_t buf_size); +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_get_melt_data_ ( + const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_EXCHANGE_RefreshData *rd, + const struct TALER_ExchangeWithdrawValues *alg_values, + struct MeltData *md); /** * Free all information associated with a melting session. Note * that we allow the melting session to be only partially initialized, * as we use this function also when freeing melt data that was not - * fully initialized (i.e. due to failures in #TALER_EXCHANGE_deserialize_melt_data_()). + * fully initialized. * - * @param md melting data to release, the pointer itself is NOT + * @param[in] md melting data to release, the pointer itself is NOT * freed (as it is typically not allocated by itself) */ void diff --git a/src/lib/exchange_api_refreshes_reveal.c b/src/lib/exchange_api_refreshes_reveal.c index f54487ae3..69c53a6c9 100644 --- a/src/lib/exchange_api_refreshes_reveal.c +++ b/src/lib/exchange_api_refreshes_reveal.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2021 Taler Systems SA + Copyright (C) 2015-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 @@ -40,11 +40,6 @@ struct TALER_EXCHANGE_RefreshesRevealHandle { /** - * The connection to exchange this request handle will use - */ - struct TALER_EXCHANGE_Handle *exchange; - - /** * The url for this request. */ char *url; @@ -61,6 +56,11 @@ struct TALER_EXCHANGE_RefreshesRevealHandle struct GNUNET_CURL_Job *job; /** + * Exchange-contributed values to the operation. + */ + struct TALER_ExchangeWithdrawValues *alg_values; + + /** * Function to call with the result. */ TALER_EXCHANGE_RefreshesRevealCallback reveal_cb; @@ -73,7 +73,7 @@ struct TALER_EXCHANGE_RefreshesRevealHandle /** * Actual information about the melt operation. */ - struct MeltData *md; + struct MeltData md; /** * The index selected by the exchange in cut-and-choose to not be revealed. @@ -84,28 +84,28 @@ struct TALER_EXCHANGE_RefreshesRevealHandle /** - * We got a 200 OK response for the /refreshes/$RCH/reveal operation. - * Extract the coin signatures and return them to the caller. - * The signatures we get from the exchange is for the blinded value. - * Thus, we first must unblind them and then should verify their - * validity. + * We got a 200 OK response for the /refreshes/$RCH/reveal operation. Extract + * the coin signatures and return them to the caller. The signatures we get + * from the exchange is for the blinded value. Thus, we first must unblind + * them and then should verify their validity. * * If everything checks out, we return the unblinded signatures * to the application via the callback. * * @param rrh operation handle * @param json reply from the exchange - * @param[out] sigs array of length `num_fresh_coins`, initialized to contain RSA signatures + * @param[out] rcis array of length `num_fresh_coins`, initialized to contain the coin data * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors */ -static int +static enum GNUNET_GenericReturnValue refresh_reveal_ok (struct TALER_EXCHANGE_RefreshesRevealHandle *rrh, const json_t *json, - struct TALER_DenominationSignature *sigs) + struct TALER_EXCHANGE_RevealedCoinInfo *rcis) { - json_t *jsona; + const json_t *jsona; struct GNUNET_JSON_Specification outer_spec[] = { - GNUNET_JSON_spec_json ("ev_sigs", &jsona), + GNUNET_JSON_spec_array_const ("ev_sigs", + &jsona), GNUNET_JSON_spec_end () }; @@ -117,38 +117,44 @@ refresh_reveal_ok (struct TALER_EXCHANGE_RefreshesRevealHandle *rrh, GNUNET_break_op (0); return GNUNET_SYSERR; } - if (! json_is_array (jsona)) - { - /* We expected an array of coins */ - GNUNET_break_op (0); - GNUNET_JSON_parse_free (outer_spec); - return GNUNET_SYSERR; - } - if (rrh->md->num_fresh_coins != json_array_size (jsona)) + if (rrh->md.num_fresh_coins != json_array_size (jsona)) { /* Number of coins generated does not match our expectation */ GNUNET_break_op (0); - GNUNET_JSON_parse_free (outer_spec); return GNUNET_SYSERR; } - for (unsigned int i = 0; i<rrh->md->num_fresh_coins; i++) + for (unsigned int i = 0; i<rrh->md.num_fresh_coins; i++) { - const struct TALER_PlanchetSecretsP *fc; - struct TALER_DenominationPublicKey *pk; + struct TALER_EXCHANGE_RevealedCoinInfo *rci = &rcis[i]; + const struct FreshCoinData *fcd = &rrh->md.fcds[i]; + const struct TALER_DenominationPublicKey *pk; json_t *jsonai; - struct GNUNET_CRYPTO_RsaSignature *blind_sig; + struct TALER_BlindedDenominationSignature blind_sig; struct TALER_CoinSpendPublicKeyP coin_pub; - struct GNUNET_HashCode coin_hash; + struct TALER_CoinPubHashP coin_hash; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_rsa_signature ("ev_sig", &blind_sig), + TALER_JSON_spec_blinded_denom_sig ("ev_sig", + &blind_sig), GNUNET_JSON_spec_end () }; struct TALER_FreshCoin coin; + union GNUNET_CRYPTO_BlindingSecretP bks; + const struct TALER_AgeCommitmentHash *pah = NULL; - fc = &rrh->md->fresh_coins[rrh->noreveal_index][i]; - pk = &rrh->md->fresh_pks[i]; + rci->ps = fcd->ps[rrh->noreveal_index]; + rci->bks = fcd->bks[rrh->noreveal_index]; + rci->age_commitment_proof = NULL; + pk = &fcd->fresh_pk; jsonai = json_array_get (jsona, i); GNUNET_assert (NULL != jsonai); + if (NULL != rrh->md.melted_coin.age_commitment_proof) + { + rci->age_commitment_proof + = fcd->age_commitment_proofs[rrh->noreveal_index]; + TALER_age_commitment_hash (&rci->age_commitment_proof->commitment, + &rci->h_age_commitment); + pah = &rci->h_age_commitment; + } if (GNUNET_OK != GNUNET_JSON_parse (jsonai, @@ -156,33 +162,41 @@ refresh_reveal_ok (struct TALER_EXCHANGE_RefreshesRevealHandle *rrh, NULL, NULL)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (outer_spec); return GNUNET_SYSERR; } + TALER_planchet_setup_coin_priv (&rci->ps, + &rrh->alg_values[i], + &rci->coin_priv); + TALER_planchet_blinding_secret_create (&rci->ps, + &rrh->alg_values[i], + &bks); /* needed to verify the signature, and we didn't store it earlier, hence recomputing it here... */ - GNUNET_CRYPTO_eddsa_key_get_public (&fc->coin_priv.eddsa_priv, + GNUNET_CRYPTO_eddsa_key_get_public (&rci->coin_priv.eddsa_priv, &coin_pub.eddsa_pub); - GNUNET_CRYPTO_hash (&coin_pub.eddsa_pub, - sizeof (struct GNUNET_CRYPTO_EcdsaPublicKey), - &coin_hash); + TALER_coin_pub_hash ( + &coin_pub, + pah, + &coin_hash); if (GNUNET_OK != - TALER_planchet_to_coin (pk, - blind_sig, - fc, - &coin_hash, - &coin)) + TALER_planchet_to_coin ( + pk, + &blind_sig, + &bks, + &rci->coin_priv, + pah, + &coin_hash, + &rrh->alg_values[i], + &coin)) { GNUNET_break_op (0); - GNUNET_CRYPTO_rsa_signature_free (blind_sig); - GNUNET_JSON_parse_free (outer_spec); + GNUNET_JSON_parse_free (spec); return GNUNET_SYSERR; } - GNUNET_CRYPTO_rsa_signature_free (blind_sig); - sigs[i] = coin.sig; + GNUNET_JSON_parse_free (spec); + rci->sig = coin.sig; } - GNUNET_JSON_parse_free (outer_spec); return GNUNET_OK; } @@ -202,96 +216,101 @@ handle_refresh_reveal_finished (void *cls, { struct TALER_EXCHANGE_RefreshesRevealHandle *rrh = cls; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_RevealResult rr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; rrh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: { - struct TALER_DenominationSignature sigs[rrh->md->num_fresh_coins]; - int ret; + struct TALER_EXCHANGE_RevealedCoinInfo rcis[rrh->md.num_fresh_coins]; + enum GNUNET_GenericReturnValue ret; - memset (sigs, 0, sizeof (sigs)); + memset (rcis, + 0, + sizeof (rcis)); ret = refresh_reveal_ok (rrh, j, - sigs); + rcis); if (GNUNET_OK != ret) { - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + rr.hr.http_status = 0; + rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; } else { + GNUNET_assert (rrh->noreveal_index < TALER_CNC_KAPPA); + rr.details.ok.num_coins = rrh->md.num_fresh_coins; + rr.details.ok.coins = rcis; rrh->reveal_cb (rrh->reveal_cb_cls, - &hr, - rrh->md->num_fresh_coins, - rrh->md->fresh_coins[rrh->noreveal_index], - sigs); + &rr); rrh->reveal_cb = NULL; } - for (unsigned int i = 0; i<rrh->md->num_fresh_coins; i++) - if (NULL != sigs[i].rsa_signature) - GNUNET_CRYPTO_rsa_signature_free (sigs[i].rsa_signature); + for (unsigned int i = 0; i<rrh->md.num_fresh_coins; i++) + { + TALER_denom_sig_free (&rcis[i].sig); + TALER_age_commitment_proof_free (rcis[i].age_commitment_proof); + } TALER_EXCHANGE_refreshes_reveal_cancel (rrh); 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 */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_CONFLICT: /* Nothing really to verify, exchange says our reveal is inconsistent with our commitment, so either side is buggy; 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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_GONE: /* Server claims key expired or has been revoked */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange refreshes reveal\n", (unsigned int) response_code, - (int) hr.ec); + (int) rr.hr.ec); break; } if (NULL != rrh->reveal_cb) rrh->reveal_cb (rrh->reveal_cb_cls, - &hr, - 0, - NULL, - NULL); + &rr); TALER_EXCHANGE_refreshes_reveal_cancel (rrh); } struct TALER_EXCHANGE_RefreshesRevealHandle * TALER_EXCHANGE_refreshes_reveal ( - struct TALER_EXCHANGE_Handle *exchange, - size_t refresh_data_length, - const char *refresh_data, + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_EXCHANGE_RefreshData *rd, + unsigned int num_coins, + const struct TALER_ExchangeWithdrawValues alg_values[static num_coins], uint32_t noreveal_index, TALER_EXCHANGE_RefreshesRevealCallback reveal_cb, void *reveal_cb_cls) @@ -302,12 +321,13 @@ TALER_EXCHANGE_refreshes_reveal ( json_t *coin_evs; json_t *reveal_obj; json_t *link_sigs; + json_t *old_age_commitment = NULL; CURL *eh; - struct GNUNET_CURL_Context *ctx; - struct MeltData *md; - struct TALER_TransferPublicKeyP transfer_pub; + struct MeltData md; char arg_str[sizeof (struct TALER_RefreshCommitmentP) * 2 + 32]; + bool send_rms = false; + GNUNET_assert (num_coins == rd->fresh_pks_len); if (noreveal_index >= TALER_CNC_KAPPA) { /* We check this here, as it would be really bad to below just @@ -317,73 +337,59 @@ TALER_EXCHANGE_refreshes_reveal ( GNUNET_break (0); return NULL; } - if (GNUNET_YES != - TEAH_handle_is_ready (exchange)) - { - GNUNET_break (0); - return NULL; - } - md = TALER_EXCHANGE_deserialize_melt_data_ (refresh_data, - refresh_data_length); - if (NULL == md) + if (GNUNET_OK != + TALER_EXCHANGE_get_melt_data_ (rms, + rd, + alg_values, + &md)) { GNUNET_break (0); return NULL; } - /* now transfer_pub */ - GNUNET_CRYPTO_ecdhe_key_get_public ( - &md->melted_coin.transfer_priv[noreveal_index].ecdhe_priv, - &transfer_pub.ecdhe_pub); - /* now new_denoms */ GNUNET_assert (NULL != (new_denoms_h = json_array ())); GNUNET_assert (NULL != (coin_evs = json_array ())); GNUNET_assert (NULL != (link_sigs = json_array ())); - for (unsigned int i = 0; i<md->num_fresh_coins; i++) + for (unsigned int i = 0; i<md.num_fresh_coins; i++) { - struct GNUNET_HashCode denom_hash; - struct TALER_PlanchetDetail pd; - struct GNUNET_HashCode c_hash; - - GNUNET_CRYPTO_rsa_public_key_hash (md->fresh_pks[i].rsa_public_key, - &denom_hash); + const struct TALER_RefreshCoinData *rcd = &md.rcd[noreveal_index][i]; + struct TALER_DenominationHashP denom_hash; + + if (GNUNET_CRYPTO_BSA_CS == + md.fcds[i].fresh_pk.bsign_pub_key->cipher) + send_rms = true; + TALER_denom_pub_hash (&md.fcds[i].fresh_pk, + &denom_hash); GNUNET_assert (0 == json_array_append_new (new_denoms_h, GNUNET_JSON_from_data_auto ( &denom_hash))); - - if (GNUNET_OK != - TALER_planchet_prepare (&md->fresh_pks[i], - &md->fresh_coins[noreveal_index][i], - &c_hash, - &pd)) - { - /* This should have been noticed during the preparation stage. */ - GNUNET_break (0); - json_decref (new_denoms_h); - json_decref (coin_evs); - return NULL; - } GNUNET_assert (0 == - json_array_append_new (coin_evs, - GNUNET_JSON_from_data (pd.coin_ev, - pd.coin_ev_size))); + json_array_append_new ( + coin_evs, + GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_planchet ( + NULL, + &rcd->blinded_planchet)))); { struct TALER_CoinSpendSignatureP link_sig; - - TALER_wallet_link_sign (&denom_hash, - &transfer_pub, - pd.coin_ev, - pd.coin_ev_size, - &md->melted_coin.coin_priv, - &link_sig); + struct TALER_BlindedCoinHashP bch; + + TALER_coin_ev_hash (&rcd->blinded_planchet, + &denom_hash, + &bch); + TALER_wallet_link_sign ( + &denom_hash, + &md.transfer_pub[noreveal_index], + &bch, + &md.melted_coin.coin_priv, + &link_sig); GNUNET_assert (0 == json_array_append_new ( link_sigs, GNUNET_JSON_from_data_auto (&link_sig))); } - GNUNET_free (pd.coin_ev); } /* build array of transfer private keys */ @@ -392,20 +398,46 @@ TALER_EXCHANGE_refreshes_reveal ( { if (j == noreveal_index) { - /* This is crucial: exclude the transfer key for the - noreval index! */ + /* This is crucial: exclude the transfer key for the noreval index! */ continue; } GNUNET_assert (0 == json_array_append_new (transfer_privs, GNUNET_JSON_from_data_auto ( - &md->melted_coin.transfer_priv[j]))); + &md.transfer_priv[j]))); + } + + /* build array of old age commitment, if applicable */ + if (NULL != rd->melt_age_commitment_proof) + { + GNUNET_assert (NULL != rd->melt_h_age_commitment); + GNUNET_assert (NULL != (old_age_commitment = json_array ())); + + for (size_t i = 0; i < rd->melt_age_commitment_proof->commitment.num; i++) + { + enum GNUNET_GenericReturnValue ret; + + ret = json_array_append_new ( + old_age_commitment, + GNUNET_JSON_from_data_auto ( + &rd->melt_age_commitment_proof->commitment.keys[i])); + GNUNET_assert (0 == ret); + } } /* build main JSON request */ reveal_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("transfer_pub", - &transfer_pub), + &md.transfer_pub[noreveal_index]), + GNUNET_JSON_pack_allow_null ( + send_rms + ? GNUNET_JSON_pack_data_auto ("rms", + rms) + : GNUNET_JSON_pack_string ("rms", + NULL)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("old_age_commitment", + old_age_commitment)), GNUNET_JSON_pack_array_steal ("transfer_privs", transfer_privs), GNUNET_JSON_pack_array_steal ("link_sigs", @@ -418,32 +450,40 @@ TALER_EXCHANGE_refreshes_reveal ( char pub_str[sizeof (struct TALER_RefreshCommitmentP) * 2]; char *end; - end = GNUNET_STRINGS_data_to_string (&md->rc, - sizeof (struct - TALER_RefreshCommitmentP), + end = GNUNET_STRINGS_data_to_string (&md.rc, + sizeof (md.rc), pub_str, sizeof (pub_str)); *end = '\0'; GNUNET_snprintf (arg_str, sizeof (arg_str), - "/refreshes/%s/reveal", + "refreshes/%s/reveal", pub_str); } /* finally, we can actually issue the request */ rrh = GNUNET_new (struct TALER_EXCHANGE_RefreshesRevealHandle); - rrh->exchange = exchange; rrh->noreveal_index = noreveal_index; rrh->reveal_cb = reveal_cb; rrh->reveal_cb_cls = reveal_cb_cls; rrh->md = md; - rrh->url = TEAH_path_to_url (rrh->exchange, - arg_str); + rrh->alg_values + = GNUNET_new_array (md.num_fresh_coins, + struct TALER_ExchangeWithdrawValues); + for (unsigned int i = 0; i<md.num_fresh_coins; i++) + TALER_denom_ewv_copy (&rrh->alg_values[i], + &alg_values[i]); + rrh->url = TALER_url_join (url, + arg_str, + NULL); if (NULL == rrh->url) { json_decref (reveal_obj); + TALER_EXCHANGE_free_melt_data_ (&md); + GNUNET_free (rrh->alg_values); GNUNET_free (rrh); return NULL; } + eh = TALER_EXCHANGE_curl_easy_get_ (rrh->url); if ( (NULL == eh) || (GNUNET_OK != @@ -455,12 +495,13 @@ TALER_EXCHANGE_refreshes_reveal ( if (NULL != eh) curl_easy_cleanup (eh); json_decref (reveal_obj); + TALER_EXCHANGE_free_melt_data_ (&md); + GNUNET_free (rrh->alg_values); GNUNET_free (rrh->url); GNUNET_free (rrh); return NULL; } json_decref (reveal_obj); - ctx = TEAH_handle_to_context (rrh->exchange); rrh->job = GNUNET_CURL_job_add2 (ctx, eh, rrh->ctx.headers, @@ -479,10 +520,12 @@ TALER_EXCHANGE_refreshes_reveal_cancel ( GNUNET_CURL_job_cancel (rrh->job); rrh->job = NULL; } + for (unsigned int i = 0; i<rrh->md.num_fresh_coins; i++) + TALER_denom_ewv_free (&rrh->alg_values[i]); + GNUNET_free (rrh->alg_values); GNUNET_free (rrh->url); TALER_curl_easy_post_finished (&rrh->ctx); - TALER_EXCHANGE_free_melt_data_ (rrh->md); /* does not free 'md' itself */ - GNUNET_free (rrh->md); + TALER_EXCHANGE_free_melt_data_ (&rrh->md); GNUNET_free (rrh); } diff --git a/src/lib/exchange_api_refund.c b/src/lib/exchange_api_refund.c index f83a2985a..9159b55f2 100644 --- a/src/lib/exchange_api_refund.c +++ b/src/lib/exchange_api_refund.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2021 Taler Systems SA + Copyright (C) 2014-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 @@ -39,9 +39,9 @@ struct TALER_EXCHANGE_RefundHandle { /** - * The connection to exchange this request handle will use + * The keys of the exchange this request handle will use */ - struct TALER_EXCHANGE_Handle *exchange; + struct TALER_EXCHANGE_Keys *keys; /** * The url for this request. @@ -70,9 +70,33 @@ struct TALER_EXCHANGE_RefundHandle void *cb_cls; /** - * Information the exchange should sign in response. + * Hash over the proposal data to identify the contract + * which is being refunded. */ - struct TALER_RefundConfirmationPS depconf; + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * The coin's public key. This is the value that must have been + * signed (blindly) by the Exchange. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * The Merchant's public key. Allows the merchant to later refund + * the transaction or to inquire about the wire transfer identifier. + */ + struct TALER_MerchantPublicKeyP merchant; + + /** + * Merchant-generated transaction ID for the refund. + */ + uint64_t rtransaction_id; + + /** + * Amount to be refunded, including refund fee charged by the + * exchange to the customer. + */ + struct TALER_Amount refund_amount; }; @@ -87,16 +111,17 @@ struct TALER_EXCHANGE_RefundHandle * @param[out] exchange_sig set to the exchange's signature * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not */ -static int +static enum GNUNET_GenericReturnValue verify_refund_signature_ok (struct TALER_EXCHANGE_RefundHandle *rh, const json_t *json, struct TALER_ExchangePublicKeyP *exchange_pub, struct TALER_ExchangeSignatureP *exchange_sig) { - const struct TALER_EXCHANGE_Keys *key_state; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("exchange_sig", exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", exchange_pub), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + exchange_pub), GNUNET_JSON_spec_end () }; @@ -108,19 +133,22 @@ verify_refund_signature_ok (struct TALER_EXCHANGE_RefundHandle *rh, GNUNET_break_op (0); return GNUNET_SYSERR; } - key_state = TALER_EXCHANGE_get_keys (rh->exchange); if (GNUNET_OK != - TALER_EXCHANGE_test_signing_key (key_state, + TALER_EXCHANGE_test_signing_key (rh->keys, exchange_pub)) { GNUNET_break_op (0); return GNUNET_SYSERR; } if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND, - &rh->depconf, - &exchange_sig->eddsa_signature, - &exchange_pub->eddsa_pub)) + TALER_exchange_online_refund_confirmation_verify ( + &rh->h_contract_terms, + &rh->coin_pub, + &rh->merchant, + rh->rtransaction_id, + &rh->refund_amount, + exchange_pub, + exchange_sig)) { GNUNET_break_op (0); return GNUNET_SYSERR; @@ -130,282 +158,6 @@ verify_refund_signature_ok (struct TALER_EXCHANGE_RefundHandle *rh, /** - * Verify that the information in the "409 Conflict" response - * from the exchange is valid and indeed shows that the refund - * amount requested is too high. - * - * @param[in,out] rh refund handle (refund fee added) - * @param json json reply with the coin transaction history - * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not - */ -static int -verify_conflict_history_ok (struct TALER_EXCHANGE_RefundHandle *rh, - const json_t *json) -{ - json_t *history; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("history", - &history), - GNUNET_JSON_spec_end () - }; - size_t len; - struct TALER_Amount dtotal; - bool have_deposit; - struct TALER_Amount rtotal; - bool have_refund; - - if (GNUNET_OK != - GNUNET_JSON_parse (json, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - len = json_array_size (history); - if (0 == len) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - have_deposit = false; - have_refund = false; - for (size_t off = 0; off<len; off++) - { - json_t *transaction; - struct TALER_Amount amount; - const char *type; - struct GNUNET_JSON_Specification spec_glob[] = { - TALER_JSON_spec_amount_any ("amount", - &amount), - GNUNET_JSON_spec_string ("type", - &type), - GNUNET_JSON_spec_end () - }; - - transaction = json_array_get (history, - off); - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec_glob, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 == strcasecmp (type, - "DEPOSIT")) - { - struct TALER_DepositRequestPS dr = { - .purpose.size = htonl (sizeof (dr)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_DEPOSIT), - .coin_pub = rh->depconf.coin_pub - }; - struct TALER_CoinSpendSignatureP sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &sig), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &dr.h_contract_terms), - GNUNET_JSON_spec_fixed_auto ("h_wire", - &dr.h_wire), - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &dr.h_denom_pub), - TALER_JSON_spec_absolute_time_nbo ("timestamp", - &dr.wallet_timestamp), - TALER_JSON_spec_absolute_time_nbo ("refund_deadline", - &dr.refund_deadline), - TALER_JSON_spec_amount_any_nbo ("deposit_fee", - &dr.deposit_fee), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &dr.merchant), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - TALER_amount_hton (&dr.amount_with_fee, - &amount); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_DEPOSIT, - &dr, - &sig.eddsa_signature, - &rh->depconf.coin_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if ( (0 != GNUNET_memcmp (&rh->depconf.h_contract_terms, - &dr.h_contract_terms)) || - (0 != GNUNET_memcmp (&rh->depconf.merchant, - &dr.merchant)) ) - { - /* deposit information is about a different merchant/contract */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (have_deposit) - { - /* this cannot really happen, but we conservatively support it anyway */ - if (GNUNET_YES != - TALER_amount_cmp_currency (&amount, - &dtotal)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - GNUNET_break (0 <= - TALER_amount_add (&dtotal, - &dtotal, - &amount)); - } - else - { - dtotal = amount; - have_deposit = true; - } - } - else if (0 == strcasecmp (type, - "REFUND")) - { - struct TALER_MerchantSignatureP sig; - struct TALER_Amount refund_fee; - struct TALER_Amount sig_amount; - struct TALER_RefundRequestPS rr = { - .purpose.size = htonl (sizeof (rr)), - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), - .coin_pub = rh->depconf.coin_pub - }; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("refund_fee", - &refund_fee), - GNUNET_JSON_spec_fixed_auto ("merchant_sig", - &sig), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &rr.h_contract_terms), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &rr.merchant), - GNUNET_JSON_spec_uint64 ("rtransaction_id", - &rr.rtransaction_id), /* Note: converted to NBO below */ - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (transaction, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (&sig_amount, - &refund_fee, - &amount)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - TALER_amount_hton (&rr.refund_amount, - &sig_amount); - rr.rtransaction_id = GNUNET_htonll (rr.rtransaction_id); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_REFUND, - &rr, - &sig.eddsa_sig, - &rr.merchant.eddsa_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if ( (0 != GNUNET_memcmp (&rh->depconf.h_contract_terms, - &rr.h_contract_terms)) || - (0 != GNUNET_memcmp (&rh->depconf.merchant, - &rr.merchant)) ) - { - /* refund is about a different merchant/contract */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (rr.rtransaction_id == rh->depconf.rtransaction_id) - { - /* Eh, this shows either a dependency failure or idempotency, - but must not happen in a conflict reply. Fail! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (have_refund) - { - if (GNUNET_YES != - TALER_amount_cmp_currency (&amount, - &rtotal)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - GNUNET_break (0 <= - TALER_amount_add (&rtotal, - &rtotal, - &amount)); - } - else - { - rtotal = amount; - have_refund = true; - } - } - else - { - /* unexpected type, new version on server? */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected type `%s' in response for exchange refund\n", - type); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - - { - struct TALER_Amount amount; - - TALER_amount_ntoh (&amount, - &rh->depconf.refund_amount); - if (have_refund) - { - if (0 > - TALER_amount_add (&rtotal, - &rtotal, - &amount)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - } - else - { - rtotal = amount; - } - } - if (-1 == TALER_amount_cmp (&dtotal, - &rtotal)) - { - /* dtotal < rtotal: good! */ - return GNUNET_OK; - } - /* this fails to prove a conflict */ - GNUNET_break_op (0); - return GNUNET_SYSERR; -} - - -/** * Verify that the information on the "412 Dependency Failed" response * from the exchange is valid and indeed shows that there is a refund * transaction ID reuse going on. @@ -414,14 +166,15 @@ verify_conflict_history_ok (struct TALER_EXCHANGE_RefundHandle *rh, * @param json json reply with the signature * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not */ -static int +static enum GNUNET_GenericReturnValue verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh, const json_t *json) { - json_t *h; + const json_t *h; json_t *e; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("history", &h), + GNUNET_JSON_spec_array_const ("history", + &h), GNUNET_JSON_spec_end () }; @@ -433,8 +186,7 @@ verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh, GNUNET_break_op (0); return GNUNET_SYSERR; } - if ( (! json_is_array (h)) || - (1 != json_array_size (h) ) ) + if (1 != json_array_size (h)) { GNUNET_break_op (0); return GNUNET_SYSERR; @@ -445,13 +197,10 @@ verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh, const char *type; struct TALER_MerchantSignatureP sig; struct TALER_Amount refund_fee; - struct TALER_RefundRequestPS rr = { - .purpose.size = htonl (sizeof (rr)), - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), - .coin_pub = rh->depconf.coin_pub - }; + struct TALER_PrivateContractHashP h_contract_terms; uint64_t rtransaction_id; - struct GNUNET_JSON_Specification spec[] = { + struct TALER_MerchantPublicKeyP merchant_pub; + struct GNUNET_JSON_Specification ispec[] = { TALER_JSON_spec_amount_any ("amount", &amount), GNUNET_JSON_spec_string ("type", @@ -461,9 +210,9 @@ verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh, GNUNET_JSON_spec_fixed_auto ("merchant_sig", &sig), GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &rr.h_contract_terms), + &h_contract_terms), GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &rr.merchant), + &merchant_pub), GNUNET_JSON_spec_uint64 ("rtransaction_id", &rtransaction_id), GNUNET_JSON_spec_end () @@ -471,31 +220,30 @@ verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh, if (GNUNET_OK != GNUNET_JSON_parse (e, - spec, + ispec, NULL, NULL)) { GNUNET_break_op (0); return GNUNET_SYSERR; } - rr.rtransaction_id = GNUNET_htonll (rtransaction_id); - TALER_amount_hton (&rr.refund_amount, - &amount); if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_REFUND, - &rr, - &sig.eddsa_sig, - &rh->depconf.merchant.eddsa_pub)) + TALER_merchant_refund_verify (&rh->coin_pub, + &h_contract_terms, + rtransaction_id, + &amount, + &merchant_pub, + &sig)) { GNUNET_break_op (0); return GNUNET_SYSERR; } - if ( (rr.rtransaction_id != rh->depconf.rtransaction_id) || - (0 != GNUNET_memcmp (&rh->depconf.h_contract_terms, - &rr.h_contract_terms)) || - (0 != GNUNET_memcmp (&rh->depconf.merchant, - &rr.merchant)) || - (0 == TALER_amount_cmp_nbo (&rh->depconf.refund_amount, - &rr.refund_amount)) ) + if ( (rtransaction_id != rh->rtransaction_id) || + (0 != GNUNET_memcmp (&rh->h_contract_terms, + &h_contract_terms)) || + (0 != GNUNET_memcmp (&rh->merchant, + &merchant_pub)) || + (0 == TALER_amount_cmp (&rh->refund_amount, + &amount)) ) { GNUNET_break_op (0); return GNUNET_SYSERR; @@ -519,37 +267,28 @@ handle_refund_finished (void *cls, const void *response) { struct TALER_EXCHANGE_RefundHandle *rh = cls; - struct TALER_ExchangePublicKeyP exchange_pub; - struct TALER_ExchangeSignatureP exchange_sig; - struct TALER_ExchangePublicKeyP *ep = NULL; - struct TALER_ExchangeSignatureP *es = NULL; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_RefundResponse rr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; rh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: if (GNUNET_OK != verify_refund_signature_ok (rh, j, - &exchange_pub, - &exchange_sig)) + &rr.details.ok.exchange_pub, + &rr.details.ok.exchange_sig)) { GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE; - } - else - { - ep = &exchange_pub; - es = &exchange_sig; + rr.hr.http_status = 0; + rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE; } break; case MHD_HTTP_BAD_REQUEST: @@ -557,42 +296,36 @@ handle_refund_finished (void *cls, (or API version conflict); also can happen if the currency differs (which we should obviously never support). Just pass JSON reply to the application */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_FORBIDDEN: /* 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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_NOT_FOUND: /* Nothing really to verify, 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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_CONFLICT: /* Requested total refunds exceed deposited amount */ - if (GNUNET_OK != - verify_conflict_history_ok (rh, - j)) - { - GNUNET_break (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE; - hr.hint = "conflict information provided by exchange is invalid"; - break; - } - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_GONE: /* Kind of normal: the money was already sent to the merchant (it was too late for the refund). */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FAILED_DEPENDENCY: + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_PRECONDITION_FAILED: if (GNUNET_OK != @@ -600,89 +333,80 @@ handle_refund_finished (void *cls, j)) { GNUNET_break (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE; - hr.hint = "failed precondition proof returned by exchange is invalid"; + rr.hr.http_status = 0; + rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE; + rr.hr.hint = "failed precondition proof returned by exchange is invalid"; break; } /* Two different refund requests were made about the same deposit, but carrying identical refund transaction ids. */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.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); + rr.hr.ec = TALER_JSON_get_error_code (j); + rr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange refund\n", (unsigned int) response_code, - hr.ec); + rr.hr.ec); break; } rh->cb (rh->cb_cls, - &hr, - ep, - es); + &rr); TALER_EXCHANGE_refund_cancel (rh); } struct TALER_EXCHANGE_RefundHandle * -TALER_EXCHANGE_refund (struct TALER_EXCHANGE_Handle *exchange, - const struct TALER_Amount *amount, - const struct GNUNET_HashCode *h_contract_terms, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - uint64_t rtransaction_id, - const struct TALER_MerchantPrivateKeyP *merchant_priv, - TALER_EXCHANGE_RefundCallback cb, - void *cb_cls) +TALER_EXCHANGE_refund ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_Amount *amount, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + uint64_t rtransaction_id, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + TALER_EXCHANGE_RefundCallback cb, + void *cb_cls) { - struct TALER_RefundRequestPS rr = { - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), - .purpose.size = htonl (sizeof (rr)), - .h_contract_terms = *h_contract_terms, - .rtransaction_id = GNUNET_htonll (rtransaction_id), - .coin_pub = *coin_pub - }; + struct TALER_MerchantPublicKeyP merchant_pub; struct TALER_MerchantSignatureP merchant_sig; struct TALER_EXCHANGE_RefundHandle *rh; - struct GNUNET_CURL_Context *ctx; json_t *refund_obj; CURL *eh; char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32]; - GNUNET_assert (GNUNET_YES == - TEAH_handle_is_ready (exchange)); GNUNET_CRYPTO_eddsa_key_get_public (&merchant_priv->eddsa_priv, - &rr.merchant.eddsa_pub); - TALER_amount_hton (&rr.refund_amount, - amount); - GNUNET_CRYPTO_eddsa_sign (&merchant_priv->eddsa_priv, - &rr, - &merchant_sig.eddsa_sig); - - + &merchant_pub.eddsa_pub); + TALER_merchant_refund_sign (coin_pub, + h_contract_terms, + rtransaction_id, + amount, + merchant_priv, + &merchant_sig); { char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; char *end; - end = GNUNET_STRINGS_data_to_string (coin_pub, - sizeof (struct - TALER_CoinSpendPublicKeyP), - pub_str, - sizeof (pub_str)); + end = GNUNET_STRINGS_data_to_string ( + coin_pub, + sizeof (struct TALER_CoinSpendPublicKeyP), + pub_str, + sizeof (pub_str)); *end = '\0'; GNUNET_snprintf (arg_str, sizeof (arg_str), - "/coins/%s/refund", + "coins/%s/refund", pub_str); } refund_obj = GNUNET_JSON_PACK ( @@ -693,30 +417,26 @@ TALER_EXCHANGE_refund (struct TALER_EXCHANGE_Handle *exchange, GNUNET_JSON_pack_uint64 ("rtransaction_id", rtransaction_id), GNUNET_JSON_pack_data_auto ("merchant_pub", - &rr.merchant), + &merchant_pub), GNUNET_JSON_pack_data_auto ("merchant_sig", &merchant_sig)); rh = GNUNET_new (struct TALER_EXCHANGE_RefundHandle); - rh->exchange = exchange; rh->cb = cb; rh->cb_cls = cb_cls; - rh->url = TEAH_path_to_url (exchange, - arg_str); + rh->url = TALER_url_join (url, + arg_str, + NULL); if (NULL == rh->url) { json_decref (refund_obj); GNUNET_free (rh); return NULL; } - rh->depconf.purpose.size = htonl (sizeof (struct TALER_RefundConfirmationPS)); - rh->depconf.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND); - rh->depconf.h_contract_terms = *h_contract_terms; - rh->depconf.coin_pub = *coin_pub; - rh->depconf.merchant = rr.merchant; - rh->depconf.rtransaction_id = GNUNET_htonll (rtransaction_id); - TALER_amount_hton (&rh->depconf.refund_amount, - amount); - + rh->h_contract_terms = *h_contract_terms; + rh->coin_pub = *coin_pub; + rh->merchant = merchant_pub; + rh->rtransaction_id = rtransaction_id; + rh->refund_amount = *amount; eh = TALER_EXCHANGE_curl_easy_get_ (rh->url); if ( (NULL == eh) || (GNUNET_OK != @@ -736,7 +456,7 @@ TALER_EXCHANGE_refund (struct TALER_EXCHANGE_Handle *exchange, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "URL for refund: `%s'\n", rh->url); - ctx = TEAH_handle_to_context (exchange); + rh->keys = TALER_EXCHANGE_keys_incref (keys); rh->job = GNUNET_CURL_job_add2 (ctx, eh, rh->ctx.headers, @@ -756,6 +476,7 @@ TALER_EXCHANGE_refund_cancel (struct TALER_EXCHANGE_RefundHandle *refund) } GNUNET_free (refund->url); TALER_curl_easy_post_finished (&refund->ctx); + TALER_EXCHANGE_keys_decref (refund->keys); GNUNET_free (refund); } diff --git a/src/lib/exchange_api_reserves_attest.c b/src/lib/exchange_api_reserves_attest.c new file mode 100644 index 000000000..d5a867114 --- /dev/null +++ b/src/lib/exchange_api_reserves_attest.c @@ -0,0 +1,365 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_reserves_attest.c + * @brief Implementation of the POST /reserves-attest/$RESERVE_PUB requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP attest codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 /reserves-attest/$RID Handle + */ +struct TALER_EXCHANGE_ReservesAttestHandle +{ + + /** + * The keys of the this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ReservesPostAttestCallback cb; + + /** + * Public key of the reserve we are querying. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Closure for @a cb. + */ + void *cb_cls; + +}; + + +/** + * We received an #MHD_HTTP_OK attest code. Handle the JSON + * response. + * + * @param rsh handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_attest_ok (struct TALER_EXCHANGE_ReservesAttestHandle *rsh, + const json_t *j) +{ + struct TALER_EXCHANGE_ReservePostAttestResult rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_OK + }; + const json_t *attributes; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("exchange_timestamp", + &rs.details.ok.exchange_time), + GNUNET_JSON_spec_timestamp ("expiration_time", + &rs.details.ok.expiration_time), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &rs.details.ok.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &rs.details.ok.exchange_pub), + GNUNET_JSON_spec_object_const ("attributes", + &attributes), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (rsh->keys, + &rs.details.ok.exchange_pub)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.hr.ec = TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE; + rsh->cb (rsh->cb_cls, + &rs); + rsh->cb = NULL; + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; + } + rs.details.ok.attributes = attributes; + if (GNUNET_OK != + TALER_exchange_online_reserve_attest_details_verify ( + rs.details.ok.exchange_time, + rs.details.ok.expiration_time, + &rsh->reserve_pub, + attributes, + &rs.details.ok.exchange_pub, + &rs.details.ok.exchange_sig)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; + } + rsh->cb (rsh->cb_cls, + &rs); + rsh->cb = NULL; + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /reserves-attest/$RID request. + * + * @param cls the `struct TALER_EXCHANGE_ReservesAttestHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_reserves_attest_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ReservesAttestHandle *rsh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_ReservePostAttestResult rs = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + rsh->job = NULL; + switch (response_code) + { + case 0: + rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + handle_reserves_attest_ok (rsh, + j)) + { + rs.hr.http_status = 0; + rs.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 */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + /* Server doesn't have the requested attributes */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.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 */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for reserves attest\n", + (unsigned int) response_code, + (int) rs.hr.ec); + break; + } + if (NULL != rsh->cb) + { + rsh->cb (rsh->cb_cls, + &rs); + rsh->cb = NULL; + } + TALER_EXCHANGE_reserves_attest_cancel (rsh); +} + + +struct TALER_EXCHANGE_ReservesAttestHandle * +TALER_EXCHANGE_reserves_attest ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ReservePrivateKeyP *reserve_priv, + unsigned int attributes_length, + const char *attributes[const static attributes_length], + TALER_EXCHANGE_ReservesPostAttestCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ReservesAttestHandle *rsh; + CURL *eh; + char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; + struct TALER_ReserveSignatureP reserve_sig; + json_t *details; + struct GNUNET_TIME_Timestamp ts; + + if (0 == attributes_length) + { + GNUNET_break (0); + return NULL; + } + details = json_array (); + GNUNET_assert (NULL != details); + for (unsigned int i = 0; i<attributes_length; i++) + { + GNUNET_assert (0 == + json_array_append_new (details, + json_string (attributes[i]))); + } + rsh = GNUNET_new (struct TALER_EXCHANGE_ReservesAttestHandle); + rsh->cb = cb; + rsh->cb_cls = cb_cls; + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &rsh->reserve_pub.eddsa_pub); + { + char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &rsh->reserve_pub, + sizeof (rsh->reserve_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves-attest/%s", + pub_str); + } + rsh->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == rsh->url) + { + json_decref (details); + GNUNET_free (rsh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (rsh->url); + if (NULL == eh) + { + GNUNET_break (0); + json_decref (details); + GNUNET_free (rsh->url); + GNUNET_free (rsh); + return NULL; + } + ts = GNUNET_TIME_timestamp_get (); + TALER_wallet_reserve_attest_request_sign (ts, + details, + reserve_priv, + &reserve_sig); + { + json_t *attest_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("reserve_sig", + &reserve_sig), + GNUNET_JSON_pack_timestamp ("request_timestamp", + ts), + GNUNET_JSON_pack_array_steal ("details", + details)); + + if (GNUNET_OK != + TALER_curl_easy_post (&rsh->post_ctx, + eh, + attest_obj)) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + json_decref (attest_obj); + GNUNET_free (rsh->url); + GNUNET_free (rsh); + return NULL; + } + json_decref (attest_obj); + } + rsh->job = GNUNET_CURL_job_add2 (ctx, + eh, + rsh->post_ctx.headers, + &handle_reserves_attest_finished, + rsh); + rsh->keys = TALER_EXCHANGE_keys_incref (keys); + return rsh; +} + + +void +TALER_EXCHANGE_reserves_attest_cancel ( + struct TALER_EXCHANGE_ReservesAttestHandle *rsh) +{ + if (NULL != rsh->job) + { + GNUNET_CURL_job_cancel (rsh->job); + rsh->job = NULL; + } + TALER_curl_easy_post_finished (&rsh->post_ctx); + TALER_EXCHANGE_keys_decref (rsh->keys); + GNUNET_free (rsh->url); + GNUNET_free (rsh); +} + + +/* end of exchange_api_reserves_attest.c */ diff --git a/src/lib/exchange_api_reserves_close.c b/src/lib/exchange_api_reserves_close.c new file mode 100644 index 000000000..a3769a22f --- /dev/null +++ b/src/lib/exchange_api_reserves_close.c @@ -0,0 +1,373 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_reserves_close.c + * @brief Implementation of the POST /reserves/$RESERVE_PUB/close requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP close codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 /reserves/$RID/close Handle + */ +struct TALER_EXCHANGE_ReservesCloseHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ReservesCloseCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Public key of the reserve we are querying. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Our signature. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * When did we make the request. + */ + struct GNUNET_TIME_Timestamp ts; + +}; + + +/** + * We received an #MHD_HTTP_OK close code. Handle the JSON + * response. + * + * @param rch handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_close_ok (struct TALER_EXCHANGE_ReservesCloseHandle *rch, + const json_t *j) +{ + struct TALER_EXCHANGE_ReserveCloseResult rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_OK, + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("wire_amount", + &rs.details.ok.wire_amount), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + rch->cb (rch->cb_cls, + &rs); + rch->cb = NULL; + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * We received an #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS close code. Handle the JSON + * response. + * + * @param rch handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_close_kyc (struct TALER_EXCHANGE_ReservesCloseHandle *rch, + const json_t *j) +{ + struct TALER_EXCHANGE_ReserveCloseResult rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ( + "h_payto", + &rs.details.unavailable_for_legal_reasons.h_payto), + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &rs.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + rch->cb (rch->cb_cls, + &rs); + rch->cb = NULL; + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /reserves/$RID/close request. + * + * @param cls the `struct TALER_EXCHANGE_ReservesCloseHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_reserves_close_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ReservesCloseHandle *rch = cls; + const json_t *j = response; + struct TALER_EXCHANGE_ReserveCloseResult rs = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + rch->job = NULL; + switch (response_code) + { + case 0: + rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + handle_reserves_close_ok (rch, + j)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.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 */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + /* Insufficient balance to inquire for reserve close */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + if (GNUNET_OK != + handle_reserves_close_kyc (rch, + j)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for reserves close\n", + (unsigned int) response_code, + (int) rs.hr.ec); + break; + } + if (NULL != rch->cb) + { + rch->cb (rch->cb_cls, + &rs); + rch->cb = NULL; + } + TALER_EXCHANGE_reserves_close_cancel (rch); +} + + +struct TALER_EXCHANGE_ReservesCloseHandle * +TALER_EXCHANGE_reserves_close ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const char *target_payto_uri, + TALER_EXCHANGE_ReservesCloseCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ReservesCloseHandle *rch; + CURL *eh; + char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; + struct TALER_PaytoHashP h_payto; + + rch = GNUNET_new (struct TALER_EXCHANGE_ReservesCloseHandle); + rch->cb = cb; + rch->cb_cls = cb_cls; + rch->ts = GNUNET_TIME_timestamp_get (); + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &rch->reserve_pub.eddsa_pub); + { + char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &rch->reserve_pub, + sizeof (rch->reserve_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves/%s/close", + pub_str); + } + rch->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == rch->url) + { + GNUNET_free (rch); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (rch->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (rch->url); + GNUNET_free (rch); + return NULL; + } + if (NULL != target_payto_uri) + TALER_payto_hash (target_payto_uri, + &h_payto); + TALER_wallet_reserve_close_sign (rch->ts, + (NULL != target_payto_uri) + ? &h_payto + : NULL, + reserve_priv, + &rch->reserve_sig); + { + json_t *close_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("payto_uri", + target_payto_uri)), + GNUNET_JSON_pack_timestamp ("request_timestamp", + rch->ts), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &rch->reserve_sig)); + + if (GNUNET_OK != + TALER_curl_easy_post (&rch->post_ctx, + eh, + close_obj)) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + json_decref (close_obj); + GNUNET_free (rch->url); + GNUNET_free (rch); + return NULL; + } + json_decref (close_obj); + } + rch->job = GNUNET_CURL_job_add2 (ctx, + eh, + rch->post_ctx.headers, + &handle_reserves_close_finished, + rch); + return rch; +} + + +void +TALER_EXCHANGE_reserves_close_cancel ( + struct TALER_EXCHANGE_ReservesCloseHandle *rch) +{ + if (NULL != rch->job) + { + GNUNET_CURL_job_cancel (rch->job); + rch->job = NULL; + } + TALER_curl_easy_post_finished (&rch->post_ctx); + GNUNET_free (rch->url); + GNUNET_free (rch); +} + + +/* end of exchange_api_reserves_close.c */ diff --git a/src/lib/exchange_api_reserves_get.c b/src/lib/exchange_api_reserves_get.c index 7f919ef80..b6980dd1d 100644 --- a/src/lib/exchange_api_reserves_get.c +++ b/src/lib/exchange_api_reserves_get.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2021 Taler Systems SA + 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 @@ -39,11 +39,6 @@ struct TALER_EXCHANGE_ReservesGetHandle { /** - * The connection to exchange this request handle will use - */ - struct TALER_EXCHANGE_Handle *exchange; - - /** * The url for this request. */ char *url; @@ -79,22 +74,19 @@ struct TALER_EXCHANGE_ReservesGetHandle * @param j JSON response * @return #GNUNET_OK on success */ -static int +static enum GNUNET_GenericReturnValue handle_reserves_get_ok (struct TALER_EXCHANGE_ReservesGetHandle *rgh, const json_t *j) { - json_t *history; - unsigned int len; - struct TALER_Amount balance; - struct TALER_Amount balance_from_history; + struct TALER_EXCHANGE_ReserveSummary rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_OK + }; struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("balance", &balance), + TALER_JSON_spec_amount_any ("balance", + &rs.details.ok.balance), GNUNET_JSON_spec_end () }; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = MHD_HTTP_OK - }; if (GNUNET_OK != GNUNET_JSON_parse (j, @@ -105,55 +97,9 @@ handle_reserves_get_ok (struct TALER_EXCHANGE_ReservesGetHandle *rgh, GNUNET_break_op (0); return GNUNET_SYSERR; } - history = json_object_get (j, - "history"); - if (NULL == history) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - len = json_array_size (history); - { - struct TALER_EXCHANGE_ReserveHistory *rhistory; - - rhistory = GNUNET_new_array (len, - struct TALER_EXCHANGE_ReserveHistory); - if (GNUNET_OK != - TALER_EXCHANGE_parse_reserve_history (rgh->exchange, - history, - &rgh->reserve_pub, - balance.currency, - &balance_from_history, - len, - rhistory)) - { - GNUNET_break_op (0); - TALER_EXCHANGE_free_reserve_history (rhistory, - len); - return GNUNET_SYSERR; - } - if (0 != - TALER_amount_cmp (&balance_from_history, - &balance)) - { - /* exchange cannot add up balances!? */ - GNUNET_break_op (0); - TALER_EXCHANGE_free_reserve_history (rhistory, - len); - return GNUNET_SYSERR; - } - if (NULL != rgh->cb) - { - rgh->cb (rgh->cb_cls, - &hr, - &balance, - len, - rhistory); - rgh->cb = NULL; - } - TALER_EXCHANGE_free_reserve_history (rhistory, - len); - } + rgh->cb (rgh->cb_cls, + &rs); + rgh->cb = NULL; return GNUNET_OK; } @@ -173,61 +119,60 @@ handle_reserves_get_finished (void *cls, { struct TALER_EXCHANGE_ReservesGetHandle *rgh = cls; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_ReserveSummary rs = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; rgh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: if (GNUNET_OK != handle_reserves_get_ok (rgh, j)) { - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + rs.hr.http_status = 0; + rs.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 */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_NOT_FOUND: /* Nothing really to verify, 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); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.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); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.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); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for reserves get\n", + "Unexpected response code %u/%d for GET %s\n", (unsigned int) response_code, - (int) hr.ec); + (int) rs.hr.ec, + rgh->url); break; } if (NULL != rgh->cb) { rgh->cb (rgh->cb_cls, - &hr, - NULL, - 0, NULL); + &rs); rgh->cb = NULL; } TALER_EXCHANGE_reserves_get_cancel (rgh); @@ -236,23 +181,20 @@ handle_reserves_get_finished (void *cls, struct TALER_EXCHANGE_ReservesGetHandle * TALER_EXCHANGE_reserves_get ( - struct TALER_EXCHANGE_Handle *exchange, + struct GNUNET_CURL_Context *ctx, + const char *url, const struct TALER_ReservePublicKeyP *reserve_pub, struct GNUNET_TIME_Relative timeout, TALER_EXCHANGE_ReservesGetCallback cb, void *cb_cls) { struct TALER_EXCHANGE_ReservesGetHandle *rgh; - struct GNUNET_CURL_Context *ctx; CURL *eh; char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 16 + 32]; + unsigned int tms + = (unsigned int) timeout.rel_value_us + / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us; - if (GNUNET_YES != - TEAH_handle_is_ready (exchange)) - { - GNUNET_break (0); - return NULL; - } { char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; char *end; @@ -260,35 +202,33 @@ TALER_EXCHANGE_reserves_get ( end = GNUNET_STRINGS_data_to_string ( reserve_pub, - sizeof (struct TALER_ReservePublicKeyP), + sizeof (*reserve_pub), pub_str, sizeof (pub_str)); *end = '\0'; GNUNET_snprintf (timeout_str, sizeof (timeout_str), - "%llu", - (unsigned long long) - (timeout.rel_value_us - / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us)); - if (GNUNET_TIME_relative_is_zero (timeout)) + "%u", + tms); + if (0 == tms) GNUNET_snprintf (arg_str, sizeof (arg_str), - "/reserves/%s", + "reserves/%s", pub_str); else GNUNET_snprintf (arg_str, sizeof (arg_str), - "/reserves/%s?timeout_ms=%s", + "reserves/%s?timeout_ms=%s", pub_str, timeout_str); } rgh = GNUNET_new (struct TALER_EXCHANGE_ReservesGetHandle); - rgh->exchange = exchange; rgh->cb = cb; rgh->cb_cls = cb_cls; rgh->reserve_pub = *reserve_pub; - rgh->url = TEAH_path_to_url (exchange, - arg_str); + rgh->url = TALER_url_join (url, + arg_str, + NULL); if (NULL == rgh->url) { GNUNET_free (rgh); @@ -302,7 +242,13 @@ TALER_EXCHANGE_reserves_get ( GNUNET_free (rgh); return NULL; } - ctx = TEAH_handle_to_context (exchange); + if (0 != tms) + { + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT_MS, + (long) (tms + 100L))); + } rgh->job = GNUNET_CURL_job_add (ctx, eh, &handle_reserves_get_finished, diff --git a/src/lib/exchange_api_reserves_get_attestable.c b/src/lib/exchange_api_reserves_get_attestable.c new file mode 100644 index 000000000..f58e0592e --- /dev/null +++ b/src/lib/exchange_api_reserves_get_attestable.c @@ -0,0 +1,276 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_reserves_get_attestable.c + * @brief Implementation of the GET_ATTESTABLE /reserves/$RESERVE_PUB requests + * @author Christian Grothoff + */ +#include "platform.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 "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 /reserves/ GET_ATTESTABLE Handle + */ +struct TALER_EXCHANGE_ReservesGetAttestHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ReservesGetAttestCallback cb; + + /** + * Public key of the reserve we are querying. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Closure for @a cb. + */ + void *cb_cls; + +}; + + +/** + * We received an #MHD_HTTP_OK status code. Handle the JSON + * response. + * + * @param rgah handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_get_attestable_ok ( + struct TALER_EXCHANGE_ReservesGetAttestHandle *rgah, + const json_t *j) +{ + struct TALER_EXCHANGE_ReserveGetAttestResult rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_OK + }; + const json_t *details; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("details", + &details), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + { + unsigned int dlen = json_array_size (details); + const char *attributes[GNUNET_NZL (dlen)]; + + for (unsigned int i = 0; i<dlen; i++) + { + json_t *detail = json_array_get (details, + i); + attributes[i] = json_string_value (detail); + if (NULL == attributes[i]) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + rs.details.ok.attributes_length = dlen; + rs.details.ok.attributes = attributes; + rgah->cb (rgah->cb_cls, + &rs); + rgah->cb = NULL; + } + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP GET /reserves-attest/$RID request. + * + * @param cls the `struct TALER_EXCHANGE_ReservesGetAttestableHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_reserves_get_attestable_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ReservesGetAttestHandle *rgah = cls; + const json_t *j = response; + struct TALER_EXCHANGE_ReserveGetAttestResult rs = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + rgah->job = NULL; + switch (response_code) + { + case 0: + rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + handle_reserves_get_attestable_ok (rgah, + j)) + { + rs.hr.http_status = 0; + rs.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 */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.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 */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for reserves get_attestable\n", + (unsigned int) response_code, + (int) rs.hr.ec); + break; + } + if (NULL != rgah->cb) + { + rgah->cb (rgah->cb_cls, + &rs); + rgah->cb = NULL; + } + TALER_EXCHANGE_reserves_get_attestable_cancel (rgah); +} + + +struct TALER_EXCHANGE_ReservesGetAttestHandle * +TALER_EXCHANGE_reserves_get_attestable ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_ReservePublicKeyP *reserve_pub, + TALER_EXCHANGE_ReservesGetAttestCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ReservesGetAttestHandle *rgah; + CURL *eh; + char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; + + { + char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + reserve_pub, + sizeof (*reserve_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves-attest/%s", + pub_str); + } + rgah = GNUNET_new (struct TALER_EXCHANGE_ReservesGetAttestHandle); + rgah->cb = cb; + rgah->cb_cls = cb_cls; + rgah->reserve_pub = *reserve_pub; + rgah->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == rgah->url) + { + GNUNET_free (rgah); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (rgah->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (rgah->url); + GNUNET_free (rgah); + return NULL; + } + rgah->job = GNUNET_CURL_job_add (ctx, + eh, + &handle_reserves_get_attestable_finished, + rgah); + return rgah; +} + + +void +TALER_EXCHANGE_reserves_get_attestable_cancel ( + struct TALER_EXCHANGE_ReservesGetAttestHandle *rgah) +{ + if (NULL != rgah->job) + { + GNUNET_CURL_job_cancel (rgah->job); + rgah->job = NULL; + } + GNUNET_free (rgah->url); + GNUNET_free (rgah); +} + + +/* end of exchange_api_reserves_get_attestable.c */ diff --git a/src/lib/exchange_api_reserves_history.c b/src/lib/exchange_api_reserves_history.c new file mode 100644 index 000000000..0654ad837 --- /dev/null +++ b/src/lib/exchange_api_reserves_history.c @@ -0,0 +1,1145 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_reserves_history.c + * @brief Implementation of the POST /reserves/$RESERVE_PUB/history requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP history codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#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 /reserves/$RID/history Handle + */ +struct TALER_EXCHANGE_ReservesHistoryHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ReservesHistoryCallback cb; + + /** + * Public key of the reserve we are querying. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Where to store the etag (if any). + */ + uint64_t etag; + +}; + + +/** + * Context for history entry helpers. + */ +struct HistoryParseContext +{ + + /** + * Keys of the exchange we use. + */ + const struct TALER_EXCHANGE_Keys *keys; + + /** + * Our reserve public key. + */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /** + * Array of UUIDs. + */ + struct GNUNET_HashCode *uuids; + + /** + * Where to sum up total inbound amounts. + */ + struct TALER_Amount *total_in; + + /** + * Where to sum up total outbound amounts. + */ + struct TALER_Amount *total_out; + + /** + * Number of entries already used in @e uuids. + */ + unsigned int uuid_off; +}; + + +/** + * Type of a function called to parse a reserve history + * entry @a rh. + * + * @param[in,out] rh where to write the result + * @param[in,out] uc UUID context for duplicate detection + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +typedef enum GNUNET_GenericReturnValue +(*ParseHelper)(struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction); + + +/** + * Parse "credit" reserve history entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_credit (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ + const char *wire_uri; + uint64_t wire_reference; + struct GNUNET_TIME_Timestamp timestamp; + struct GNUNET_JSON_Specification withdraw_spec[] = { + GNUNET_JSON_spec_uint64 ("wire_reference", + &wire_reference), + GNUNET_JSON_spec_timestamp ("timestamp", + ×tamp), + TALER_JSON_spec_payto_uri ("sender_account_url", + &wire_uri), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_CREDIT; + if (0 > + TALER_amount_add (uc->total_in, + uc->total_in, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + withdraw_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + rh->details.in_details.sender_url = GNUNET_strdup (wire_uri); + rh->details.in_details.wire_reference = wire_reference; + rh->details.in_details.timestamp = timestamp; + return GNUNET_OK; +} + + +/** + * Parse "credit" reserve history entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ + struct TALER_ReserveSignatureP sig; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_BlindedCoinHashP bch; + struct TALER_Amount withdraw_fee; + struct GNUNET_JSON_Specification withdraw_spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &sig), + TALER_JSON_spec_amount_any ("withdraw_fee", + &withdraw_fee), + GNUNET_JSON_spec_fixed_auto ("h_denom_pub", + &h_denom_pub), + GNUNET_JSON_spec_fixed_auto ("h_coin_envelope", + &bch), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_WITHDRAWAL; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + withdraw_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + /* Check that the signature is a valid withdraw request */ + if (GNUNET_OK != + TALER_wallet_withdraw_verify (&h_denom_pub, + &rh->amount, + &bch, + uc->reserve_pub, + &sig)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } + /* check that withdraw fee matches expectations! */ + { + const struct TALER_EXCHANGE_Keys *key_state; + const struct TALER_EXCHANGE_DenomPublicKey *dki; + + key_state = uc->keys; + dki = TALER_EXCHANGE_get_denomination_key_by_hash (key_state, + &h_denom_pub); + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&withdraw_fee, + &dki->fees.withdraw)) || + (0 != + TALER_amount_cmp (&withdraw_fee, + &dki->fees.withdraw)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } + rh->details.withdraw.fee = withdraw_fee; + } + rh->details.withdraw.out_authorization_sig + = json_object_get (transaction, + "signature"); + /* Check check that the same withdraw transaction + isn't listed twice by the exchange. We use the + "uuid" array to remember the hashes of all + signatures, and compare the hashes to find + duplicates. */ + GNUNET_CRYPTO_hash (&sig, + sizeof (sig), + &uc->uuids[uc->uuid_off]); + for (unsigned int i = 0; i<uc->uuid_off; i++) + { + if (0 == GNUNET_memcmp (&uc->uuids[uc->uuid_off], + &uc->uuids[i])) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } + } + uc->uuid_off++; + + if (0 > + TALER_amount_add (uc->total_out, + uc->total_out, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Parse "recoup" reserve history entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_recoup (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ + const struct TALER_EXCHANGE_Keys *key_state; + struct GNUNET_JSON_Specification recoup_spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &rh->details.recoup_details.coin_pub), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &rh->details.recoup_details.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &rh->details.recoup_details.exchange_pub), + GNUNET_JSON_spec_timestamp ("timestamp", + &rh->details.recoup_details.timestamp), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_RECOUP; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + recoup_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + key_state = uc->keys; + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (key_state, + &rh->details. + recoup_details.exchange_pub)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_exchange_online_confirm_recoup_verify ( + rh->details.recoup_details.timestamp, + &rh->amount, + &rh->details.recoup_details.coin_pub, + uc->reserve_pub, + &rh->details.recoup_details.exchange_pub, + &rh->details.recoup_details.exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (uc->total_in, + uc->total_in, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Parse "closing" reserve history entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_closing (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ + const struct TALER_EXCHANGE_Keys *key_state; + struct GNUNET_JSON_Specification closing_spec[] = { + TALER_JSON_spec_payto_uri ( + "receiver_account_details", + &rh->details.close_details.receiver_account_details), + GNUNET_JSON_spec_fixed_auto ("wtid", + &rh->details.close_details.wtid), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &rh->details.close_details.exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &rh->details.close_details.exchange_pub), + TALER_JSON_spec_amount_any ("closing_fee", + &rh->details.close_details.fee), + GNUNET_JSON_spec_timestamp ("timestamp", + &rh->details.close_details.timestamp), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_CLOSING; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + closing_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + key_state = uc->keys; + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key ( + key_state, + &rh->details.close_details.exchange_pub)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_exchange_online_reserve_closed_verify ( + rh->details.close_details.timestamp, + &rh->amount, + &rh->details.close_details.fee, + rh->details.close_details.receiver_account_details, + &rh->details.close_details.wtid, + uc->reserve_pub, + &rh->details.close_details.exchange_pub, + &rh->details.close_details.exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (uc->total_out, + uc->total_out, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Parse "merge" reserve history entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_merge (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ + uint32_t flags32; + struct GNUNET_JSON_Specification merge_spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &rh->details.merge_details.h_contract_terms), + GNUNET_JSON_spec_fixed_auto ("merge_pub", + &rh->details.merge_details.merge_pub), + GNUNET_JSON_spec_fixed_auto ("purse_pub", + &rh->details.merge_details.purse_pub), + GNUNET_JSON_spec_uint32 ("min_age", + &rh->details.merge_details.min_age), + GNUNET_JSON_spec_uint32 ("flags", + &flags32), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rh->details.merge_details.reserve_sig), + TALER_JSON_spec_amount_any ("purse_fee", + &rh->details.merge_details.purse_fee), + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &rh->details.merge_details.merge_timestamp), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &rh->details.merge_details.purse_expiration), + GNUNET_JSON_spec_bool ("merged", + &rh->details.merge_details.merged), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_MERGE; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + merge_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + rh->details.merge_details.flags = + (enum TALER_WalletAccountMergeFlags) flags32; + if (GNUNET_OK != + TALER_wallet_account_merge_verify ( + rh->details.merge_details.merge_timestamp, + &rh->details.merge_details.purse_pub, + rh->details.merge_details.purse_expiration, + &rh->details.merge_details.h_contract_terms, + &rh->amount, + &rh->details.merge_details.purse_fee, + rh->details.merge_details.min_age, + rh->details.merge_details.flags, + uc->reserve_pub, + &rh->details.merge_details.reserve_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (rh->details.merge_details.merged) + { + if (0 > + TALER_amount_add (uc->total_in, + uc->total_in, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + else + { + if (0 > + TALER_amount_add (uc->total_out, + uc->total_out, + &rh->details.merge_details.purse_fee)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Parse "open" reserve open entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_open (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ + struct GNUNET_JSON_Specification open_spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rh->details.open_request.reserve_sig), + TALER_JSON_spec_amount_any ("open_payment", + &rh->details.open_request.reserve_payment), + GNUNET_JSON_spec_uint32 ("requested_min_purses", + &rh->details.open_request.purse_limit), + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rh->details.open_request.request_timestamp), + GNUNET_JSON_spec_timestamp ("requested_expiration", + &rh->details.open_request.reserve_expiration), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_OPEN; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + open_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_wallet_reserve_open_verify ( + &rh->amount, + rh->details.open_request.request_timestamp, + rh->details.open_request.reserve_expiration, + rh->details.open_request.purse_limit, + uc->reserve_pub, + &rh->details.open_request.reserve_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (uc->total_out, + uc->total_out, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Parse "close" reserve close entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_close (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ + struct GNUNET_JSON_Specification close_spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rh->details.close_request.reserve_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_payto", + &rh->details.close_request. + target_account_h_payto), + NULL), + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rh->details.close_request.request_timestamp), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_CLOSE; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + close_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + /* force amount to invalid */ + memset (&rh->amount, + 0, + sizeof (rh->amount)); + if (GNUNET_OK != + TALER_wallet_reserve_close_verify ( + rh->details.close_request.request_timestamp, + &rh->details.close_request.target_account_h_payto, + uc->reserve_pub, + &rh->details.close_request.reserve_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +static void +free_reserve_history ( + unsigned int len, + struct TALER_EXCHANGE_ReserveHistoryEntry rhistory[static len]) +{ + for (unsigned int i = 0; i<len; i++) + { + switch (rhistory[i].type) + { + case TALER_EXCHANGE_RTT_CREDIT: + GNUNET_free (rhistory[i].details.in_details.sender_url); + break; + case TALER_EXCHANGE_RTT_WITHDRAWAL: + break; + case TALER_EXCHANGE_RTT_AGEWITHDRAWAL: + break; + case TALER_EXCHANGE_RTT_RECOUP: + break; + case TALER_EXCHANGE_RTT_CLOSING: + break; + case TALER_EXCHANGE_RTT_MERGE: + break; + case TALER_EXCHANGE_RTT_OPEN: + break; + case TALER_EXCHANGE_RTT_CLOSE: + break; + } + } + GNUNET_free (rhistory); +} + + +/** + * Parse history given in JSON format and return it in binary + * format. + * + * @param keys exchange keys + * @param history JSON array with the history + * @param reserve_pub public key of the reserve to inspect + * @param currency currency we expect the balance to be in + * @param[out] total_in set to value of credits to reserve + * @param[out] total_out set to value of debits from reserve + * @param history_length number of entries in @a history + * @param[out] rhistory array of length @a history_length, set to the + * parsed history entries + * @return #GNUNET_OK if history was valid and @a rhistory and @a balance + * were set, + * #GNUNET_SYSERR if there was a protocol violation in @a history + */ +static enum GNUNET_GenericReturnValue +parse_reserve_history ( + const struct TALER_EXCHANGE_Keys *keys, + const json_t *history, + const struct TALER_ReservePublicKeyP *reserve_pub, + const char *currency, + struct TALER_Amount *total_in, + struct TALER_Amount *total_out, + unsigned int history_length, + struct TALER_EXCHANGE_ReserveHistoryEntry rhistory[static history_length]) +{ + const struct + { + const char *type; + ParseHelper helper; + } map[] = { + { "CREDIT", &parse_credit }, + { "WITHDRAW", &parse_withdraw }, + { "RECOUP", &parse_recoup }, + { "MERGE", &parse_merge }, + { "CLOSING", &parse_closing }, + { "OPEN", &parse_open }, + { "CLOSE", &parse_close }, + { NULL, NULL } + }; + struct GNUNET_HashCode uuid[history_length]; + struct HistoryParseContext uc = { + .keys = keys, + .reserve_pub = reserve_pub, + .uuids = uuid, + .total_in = total_in, + .total_out = total_out + }; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (currency, + total_in)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (currency, + total_out)); + for (unsigned int off = 0; off<history_length; off++) + { + struct TALER_EXCHANGE_ReserveHistoryEntry *rh = &rhistory[off]; + json_t *transaction; + struct TALER_Amount amount; + const char *type; + struct GNUNET_JSON_Specification hist_spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + TALER_JSON_spec_amount_any ("amount", + &amount), + /* 'wire' and 'signature' are optional depending on 'type'! */ + GNUNET_JSON_spec_end () + }; + bool found = false; + + transaction = json_array_get (history, + off); + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + hist_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_dumpf (transaction, + stderr, + JSON_INDENT (2)); + return GNUNET_SYSERR; + } + rh->amount = amount; + if (GNUNET_YES != + TALER_amount_cmp_currency (&amount, + total_in)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + for (unsigned int i = 0; NULL != map[i].type; i++) + { + if (0 == strcasecmp (map[i].type, + type)) + { + found = true; + if (GNUNET_OK != + map[i].helper (rh, + &uc, + transaction)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + break; + } + } + if (! found) + { + /* unexpected 'type', protocol incompatibility, complain! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Handle HTTP header received by curl. + * + * @param buffer one line of HTTP header data + * @param size size of an item + * @param nitems number of items passed + * @param userdata our `struct TALER_EXCHANGE_ReservesHistoryHandle *` + * @return `size * nitems` + */ +static size_t +handle_header (char *buffer, + size_t size, + size_t nitems, + void *userdata) +{ + struct TALER_EXCHANGE_ReservesHistoryHandle *rhh = userdata; + size_t total = size * nitems; + char *ndup; + const char *hdr_type; + char *hdr_val; + char *sp; + + ndup = GNUNET_strndup (buffer, + total); + hdr_type = strtok_r (ndup, + ":", + &sp); + if (NULL == hdr_type) + { + GNUNET_free (ndup); + return total; + } + hdr_val = strtok_r (NULL, + "\n\r", + &sp); + if (NULL == hdr_val) + { + GNUNET_free (ndup); + return total; + } + if (' ' == *hdr_val) + hdr_val++; + if (0 == strcasecmp (hdr_type, + MHD_HTTP_HEADER_ETAG)) + { + unsigned long long tval; + char dummy; + + if (1 != + sscanf (hdr_val, + "\"%llu\"%c", + &tval, + &dummy)) + { + GNUNET_break_op (0); + GNUNET_free (ndup); + return 0; + } + rhh->etag = (uint64_t) tval; + } + GNUNET_free (ndup); + return total; +} + + +/** + * We received an #MHD_HTTP_OK history code. Handle the JSON + * response. + * + * @param rsh handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_history_ok (struct TALER_EXCHANGE_ReservesHistoryHandle *rsh, + const json_t *j) +{ + const json_t *history; + unsigned int len; + struct TALER_EXCHANGE_ReserveHistory rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_OK, + .details.ok.etag = rsh->etag + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("balance", + &rs.details.ok.balance), + GNUNET_JSON_spec_array_const ("history", + &history), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + len = json_array_size (history); + { + struct TALER_EXCHANGE_ReserveHistoryEntry *rhistory; + + rhistory = GNUNET_new_array (len, + struct TALER_EXCHANGE_ReserveHistoryEntry); + if (GNUNET_OK != + parse_reserve_history (rsh->keys, + history, + &rsh->reserve_pub, + rs.details.ok.balance.currency, + &rs.details.ok.total_in, + &rs.details.ok.total_out, + len, + rhistory)) + { + GNUNET_break_op (0); + free_reserve_history (len, + rhistory); + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; + } + if (NULL != rsh->cb) + { + rs.details.ok.history = rhistory; + rs.details.ok.history_len = len; + rsh->cb (rsh->cb_cls, + &rs); + rsh->cb = NULL; + } + free_reserve_history (len, + rhistory); + } + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /reserves/$RID/history request. + * + * @param cls the `struct TALER_EXCHANGE_ReservesHistoryHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_reserves_history_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ReservesHistoryHandle *rsh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_ReserveHistory rs = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + rsh->job = NULL; + switch (response_code) + { + case 0: + rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + handle_reserves_history_ok (rsh, + j)) + { + rs.hr.http_status = 0; + rs.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 */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.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 */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for reserves history\n", + (unsigned int) response_code, + (int) rs.hr.ec); + break; + } + if (NULL != rsh->cb) + { + rsh->cb (rsh->cb_cls, + &rs); + rsh->cb = NULL; + } + TALER_EXCHANGE_reserves_history_cancel (rsh); +} + + +struct TALER_EXCHANGE_ReservesHistoryHandle * +TALER_EXCHANGE_reserves_history ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ReservePrivateKeyP *reserve_priv, + uint64_t start_off, + TALER_EXCHANGE_ReservesHistoryCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ReservesHistoryHandle *rsh; + CURL *eh; + char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 64]; + struct curl_slist *job_headers; + + rsh = GNUNET_new (struct TALER_EXCHANGE_ReservesHistoryHandle); + rsh->cb = cb; + rsh->cb_cls = cb_cls; + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &rsh->reserve_pub.eddsa_pub); + { + char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &rsh->reserve_pub, + sizeof (rsh->reserve_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + if (0 != start_off) + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves/%s/history?start=%llu", + pub_str, + (unsigned long long) start_off); + else + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves/%s/history", + pub_str); + } + rsh->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == rsh->url) + { + GNUNET_free (rsh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (rsh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (rsh->url); + GNUNET_free (rsh); + return NULL; + } + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &handle_header)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + rsh)); + { + struct TALER_ReserveSignatureP reserve_sig; + char *sig_hdr; + char *hdr; + + TALER_wallet_reserve_history_sign (start_off, + reserve_priv, + &reserve_sig); + + sig_hdr = GNUNET_STRINGS_data_to_string_alloc ( + &reserve_sig, + sizeof (reserve_sig)); + GNUNET_asprintf (&hdr, + "%s: %s", + TALER_RESERVE_HISTORY_SIGNATURE_HEADER, + sig_hdr); + GNUNET_free (sig_hdr); + job_headers = curl_slist_append (NULL, + hdr); + GNUNET_free (hdr); + if (NULL == job_headers) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + return NULL; + } + } + + rsh->keys = TALER_EXCHANGE_keys_incref (keys); + rsh->job = GNUNET_CURL_job_add2 (ctx, + eh, + job_headers, + &handle_reserves_history_finished, + rsh); + curl_slist_free_all (job_headers); + return rsh; +} + + +void +TALER_EXCHANGE_reserves_history_cancel ( + struct TALER_EXCHANGE_ReservesHistoryHandle *rsh) +{ + if (NULL != rsh->job) + { + GNUNET_CURL_job_cancel (rsh->job); + rsh->job = NULL; + } + TALER_curl_easy_post_finished (&rsh->post_ctx); + GNUNET_free (rsh->url); + TALER_EXCHANGE_keys_decref (rsh->keys); + GNUNET_free (rsh); +} + + +/* end of exchange_api_reserves_history.c */ diff --git a/src/lib/exchange_api_reserves_open.c b/src/lib/exchange_api_reserves_open.c new file mode 100644 index 000000000..36e435685 --- /dev/null +++ b/src/lib/exchange_api_reserves_open.c @@ -0,0 +1,567 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_reserves_open.c + * @brief Implementation of the POST /reserves/$RESERVE_PUB/open requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP open codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_exchange_service.h" +#include "taler_json_lib.h" +#include "exchange_api_common.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + + +/** + * Information we keep per coin to validate the reply. + */ +struct CoinData +{ + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature by the coin. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * The hash of the denomination's public key + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * How much did this coin contribute. + */ + struct TALER_Amount contribution; +}; + + +/** + * @brief A /reserves/$RID/open Handle + */ +struct TALER_EXCHANGE_ReservesOpenHandle +{ + + /** + * The keys of the exchange this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ReservesOpenCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Information we keep per coin to validate the reply. + */ + struct CoinData *coins; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + + /** + * Public key of the reserve we are querying. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Our signature. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * When did we make the request. + */ + struct GNUNET_TIME_Timestamp ts; + +}; + + +/** + * We received an #MHD_HTTP_OK open code. Handle the JSON + * response. + * + * @param roh handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_open_ok (struct TALER_EXCHANGE_ReservesOpenHandle *roh, + const json_t *j) +{ + struct TALER_EXCHANGE_ReserveOpenResult rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_OK, + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("open_cost", + &rs.details.ok.open_cost), + GNUNET_JSON_spec_timestamp ("reserve_expiration", + &rs.details.ok.expiration_time), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + roh->cb (roh->cb_cls, + &rs); + roh->cb = NULL; + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * We received an #MHD_HTTP_PAYMENT_REQUIRED open code. Handle the JSON + * response. + * + * @param roh handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_open_pr (struct TALER_EXCHANGE_ReservesOpenHandle *roh, + const json_t *j) +{ + struct TALER_EXCHANGE_ReserveOpenResult rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_PAYMENT_REQUIRED, + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("open_cost", + &rs.details.payment_required.open_cost), + GNUNET_JSON_spec_timestamp ("reserve_expiration", + &rs.details.payment_required.expiration_time), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + roh->cb (roh->cb_cls, + &rs); + roh->cb = NULL; + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * We received an #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS open code. Handle the JSON + * response. + * + * @param roh handle of the request + * @param j JSON response + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +handle_reserves_open_kyc (struct TALER_EXCHANGE_ReservesOpenHandle *roh, + const json_t *j) +{ + struct TALER_EXCHANGE_ReserveOpenResult rs = { + .hr.reply = j, + .hr.http_status = MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ( + "h_payto", + &rs.details.unavailable_for_legal_reasons.h_payto), + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &rs.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + roh->cb (roh->cb_cls, + &rs); + roh->cb = NULL; + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /reserves/$RID/open request. + * + * @param cls the `struct TALER_EXCHANGE_ReservesOpenHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_reserves_open_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ReservesOpenHandle *roh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_ReserveOpenResult rs = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + roh->job = NULL; + switch (response_code) + { + case 0: + rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + handle_reserves_open_ok (roh, + j)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.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 */ + GNUNET_break (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_PAYMENT_REQUIRED: + if (GNUNET_OK != + handle_reserves_open_pr (roh, + j)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + break; + case MHD_HTTP_FORBIDDEN: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + GNUNET_break (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + { + const struct CoinData *cd = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &rs.details.conflict.coin_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + for (unsigned int i = 0; i<roh->num_coins; i++) + { + const struct CoinData *cdi = &roh->coins[i]; + + if (0 == GNUNET_memcmp (&rs.details.conflict.coin_pub, + &cdi->coin_pub)) + { + cd = cdi; + break; + } + } + if (NULL == cd) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + } + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + if (GNUNET_OK != + handle_reserves_open_kyc (roh, + j)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + rs.hr.ec = TALER_JSON_get_error_code (j); + rs.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for reserves open\n", + (unsigned int) response_code, + (int) rs.hr.ec); + break; + } + if (NULL != roh->cb) + { + roh->cb (roh->cb_cls, + &rs); + roh->cb = NULL; + } + TALER_EXCHANGE_reserves_open_cancel (roh); +} + + +struct TALER_EXCHANGE_ReservesOpenHandle * +TALER_EXCHANGE_reserves_open ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_Amount *reserve_contribution, + unsigned int coin_payments_length, + const struct TALER_EXCHANGE_PurseDeposit coin_payments[ + static coin_payments_length], + struct GNUNET_TIME_Timestamp expiration_time, + uint32_t min_purses, + TALER_EXCHANGE_ReservesOpenCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ReservesOpenHandle *roh; + CURL *eh; + char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; + json_t *cpa; + + roh = GNUNET_new (struct TALER_EXCHANGE_ReservesOpenHandle); + roh->cb = cb; + roh->cb_cls = cb_cls; + roh->ts = GNUNET_TIME_timestamp_get (); + GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, + &roh->reserve_pub.eddsa_pub); + { + char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &roh->reserve_pub, + sizeof (roh->reserve_pub), + pub_str, + sizeof (pub_str)); + *end = '\0'; + GNUNET_snprintf (arg_str, + sizeof (arg_str), + "reserves/%s/open", + pub_str); + } + roh->url = TALER_url_join (url, + arg_str, + NULL); + if (NULL == roh->url) + { + GNUNET_free (roh); + return NULL; + } + eh = TALER_EXCHANGE_curl_easy_get_ (roh->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (roh->url); + GNUNET_free (roh); + return NULL; + } + TALER_wallet_reserve_open_sign (reserve_contribution, + roh->ts, + expiration_time, + min_purses, + reserve_priv, + &roh->reserve_sig); + roh->coins = GNUNET_new_array (coin_payments_length, + struct CoinData); + cpa = json_array (); + GNUNET_assert (NULL != cpa); + for (unsigned int i = 0; i<coin_payments_length; i++) + { + const struct TALER_EXCHANGE_PurseDeposit *pd = &coin_payments[i]; + const struct TALER_AgeCommitmentProof *acp = pd->age_commitment_proof; + struct TALER_AgeCommitmentHash ahac; + struct TALER_AgeCommitmentHash *achp = NULL; + struct CoinData *cd = &roh->coins[i]; + json_t *cp; + + cd->contribution = pd->amount; + cd->h_denom_pub = pd->h_denom_pub; + if (NULL != acp) + { + TALER_age_commitment_hash (&acp->commitment, + &ahac); + achp = &ahac; + } + TALER_wallet_reserve_open_deposit_sign (&pd->amount, + &roh->reserve_sig, + &pd->coin_priv, + &cd->coin_sig); + GNUNET_CRYPTO_eddsa_key_get_public (&pd->coin_priv.eddsa_priv, + &cd->coin_pub.eddsa_pub); + + cp = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + achp)), + TALER_JSON_pack_amount ("amount", + &pd->amount), + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &pd->h_denom_pub), + TALER_JSON_pack_denom_sig ("ub_sig", + &pd->denom_sig), + GNUNET_JSON_pack_data_auto ("coin_pub", + &cd->coin_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &cd->coin_sig)); + GNUNET_assert (0 == + json_array_append_new (cpa, + cp)); + } + { + json_t *open_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("request_timestamp", + roh->ts), + GNUNET_JSON_pack_timestamp ("reserve_expiration", + expiration_time), + GNUNET_JSON_pack_array_steal ("payments", + cpa), + TALER_JSON_pack_amount ("reserve_payment", + reserve_contribution), + GNUNET_JSON_pack_uint64 ("purse_limit", + min_purses), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &roh->reserve_sig)); + + if (GNUNET_OK != + TALER_curl_easy_post (&roh->post_ctx, + eh, + open_obj)) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + json_decref (open_obj); + GNUNET_free (roh->coins); + GNUNET_free (roh->url); + GNUNET_free (roh); + return NULL; + } + json_decref (open_obj); + } + roh->keys = TALER_EXCHANGE_keys_incref (keys); + roh->job = GNUNET_CURL_job_add2 (ctx, + eh, + roh->post_ctx.headers, + &handle_reserves_open_finished, + roh); + return roh; +} + + +void +TALER_EXCHANGE_reserves_open_cancel ( + struct TALER_EXCHANGE_ReservesOpenHandle *roh) +{ + if (NULL != roh->job) + { + GNUNET_CURL_job_cancel (roh->job); + roh->job = NULL; + } + TALER_curl_easy_post_finished (&roh->post_ctx); + GNUNET_free (roh->coins); + GNUNET_free (roh->url); + TALER_EXCHANGE_keys_decref (roh->keys); + GNUNET_free (roh); +} + + +/* end of exchange_api_reserves_open.c */ diff --git a/src/lib/exchange_api_stefan.c b/src/lib/exchange_api_stefan.c new file mode 100644 index 000000000..226bca82f --- /dev/null +++ b/src/lib/exchange_api_stefan.c @@ -0,0 +1,328 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_stefan.c + * @brief calculations on the STEFAN curve + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "exchange_api_handle.h" +#include <math.h> + + +/** + * Determine smallest denomination in @a keys. + * + * @param keys exchange response to evaluate + * @return NULL on error (no denominations) + */ +static const struct TALER_Amount * +get_unit (const struct TALER_EXCHANGE_Keys *keys) +{ + const struct TALER_Amount *min = NULL; + + for (unsigned int i = 0; i<keys->num_denom_keys; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dk + = &keys->denom_keys[i]; + + if ( (NULL == min) || + (1 == TALER_amount_cmp (min, + /* > */ + &dk->value)) ) + min = &dk->value; + } + GNUNET_break (NULL != min); + return min; +} + + +/** + * Convert amount to double for STEFAN curve evaluation. + * + * @param a input amount + * @return (rounded) amount as a double + */ +static double +amount_to_double (const struct TALER_Amount *a) +{ + double d = (double) a->value; + + d += a->fraction / ((double) TALER_AMOUNT_FRAC_BASE); + return d; +} + + +/** + * Convert double to amount for STEFAN curve evaluation. + * + * @param dv input amount + * @param currency deisred currency + * @param[out] rval (rounded) amount as a double + */ +static void +double_to_amount (double dv, + const char *currency, + struct TALER_Amount *rval) +{ + double rem; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (currency, + rval)); + rval->value = floorl (dv); + rem = dv - ((double) rval->value); + if (rem < 0.0) + rem = 0.0; + rem *= TALER_AMOUNT_FRAC_BASE; + rval->fraction = floorl (rem); + if (rval->fraction >= TALER_AMOUNT_FRAC_BASE) + { + /* Strange, multiplication overflowed our range, + round up value instead */ + rval->fraction = 0; + rval->value += 1; + } +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_keys_stefan_b2n ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_Amount *brut, + struct TALER_Amount *net) +{ + const struct TALER_Amount *min; + double log_d = amount_to_double (&keys->stefan_log); + double lin_d = keys->stefan_lin; + double abs_d = amount_to_double (&keys->stefan_abs); + double bru_d = amount_to_double (brut); + double min_d; + double fee_d; + double net_d; + + if (TALER_amount_is_zero (brut)) + { + *net = *brut; + return GNUNET_NO; + } + min = get_unit (keys); + if (NULL == min) + return GNUNET_SYSERR; + if (1.0f <= keys->stefan_lin) + { + /* This cannot work, linear STEFAN fee estimate always + exceed any gross amount. */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + min_d = amount_to_double (min); + fee_d = abs_d + + log_d * log2 (bru_d / min_d) + + lin_d * bru_d; + if (fee_d > bru_d) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (brut->currency, + net)); + return GNUNET_NO; + } + net_d = bru_d - fee_d; + double_to_amount (net_d, + brut->currency, + net); + return GNUNET_OK; +} + + +/** + * Our function + * f(x) := ne + ab + lo * log2(x/mi) + li * x - x + * for #newton(). + */ +static double +eval_f (double mi, + double ab, + double lo, + double li, + double ne, + double x) +{ + return ne + ab + lo * log2 (x / mi) + li * x - x; +} + + +/** + * Our function + * f'(x) := lo / log(2) / x + li - 1 + * for #newton(). + */ +static double +eval_fp (double mi, + double lo, + double li, + double ne, + double x) +{ + return lo / log (2) / x + li - 1; +} + + +/** + * Use Newton's method to find x where f(x)=0. + * + * @return x where "eval_f(x)==0". + */ +static double +newton (double mi, + double ab, + double lo, + double li, + double ne) +{ + const double eps = 0.00000001; /* max error allowed */ + double min_ab = ne + ab; /* result cannot be smaller than this! */ + /* compute lower bounds by various heuristics */ + double min_ab_li = min_ab + min_ab * li; + double min_ab_li_lo = min_ab_li + log2 (min_ab_li / mi) * lo; + double min_ab_lo = min_ab + log2 (min_ab / mi) * lo; + double min_ab_lo_li = min_ab_lo + min_ab_lo * li; + /* take global lower bound */ + double x_min = GNUNET_MAX (min_ab_lo_li, + min_ab_li_lo); + double x = x_min; /* use lower bound as starting point */ + + /* Objective: invert + ne := br - ab - lo * log2 (br/mi) - li * br + to find 'br'. + Method: use Newton's method to find root of: + f(x) := ne + ab + lo * log2 (x/mi) + li * x - x + using also + f'(x) := lo / log(2) / x + li - 1 + */ + /* Loop to abort in case of divergence; + 100 is already very high, 2-4 is normal! */ + for (unsigned int i = 0; i<100; i++) + { + double fx = eval_f (mi, ab, lo, li, ne, x); + double fxp = eval_fp (mi, lo, li, ne, x); + double x_new = x - fx / fxp; + + if (fabs (x - x_new) <= eps) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Needed %u rounds from %f to result BRUT %f => NET: %f\n", + i, + x_min, + x_new, + x_new - ab - li * x_new - lo * log2 (x / mi)); + return x_new; + } + if (x_new < x_min) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Divergence, obtained very bad estimate %f after %u rounds!\n", + x_new, + i); + return x_min; + } + x = x_new; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Slow convergence, returning bad estimate %f!\n", + x); + return x; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_keys_stefan_n2b ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_Amount *net, + struct TALER_Amount *brut) +{ + const struct TALER_Amount *min; + double lin_d = keys->stefan_lin; + double log_d = amount_to_double (&keys->stefan_log); + double abs_d = amount_to_double (&keys->stefan_abs); + double net_d = amount_to_double (net); + double min_d; + double brut_d; + + if (TALER_amount_is_zero (net)) + { + *brut = *net; + return GNUNET_NO; + } + min = get_unit (keys); + if (NULL == min) + return GNUNET_SYSERR; + if (1.0f <= keys->stefan_lin) + { + /* This cannot work, linear STEFAN fee estimate always + exceed any gross amount. */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + min_d = amount_to_double (min); + brut_d = newton (min_d, + abs_d, + log_d, + lin_d, + net_d); + double_to_amount (brut_d, + net->currency, + brut); + return GNUNET_OK; +} + + +void +TALER_EXCHANGE_keys_stefan_round ( + const struct TALER_EXCHANGE_Keys *keys, + struct TALER_Amount *val) +{ + const struct TALER_Amount *min; + uint32_t mod; + uint32_t frac; + uint32_t lim; + + if (0 == val->fraction) + { + /* rounding of non-fractions not supported */ + return; + } + min = get_unit (keys); + if (NULL == min) + return; + if (0 == min->fraction) + { + frac = TALER_AMOUNT_FRAC_BASE; + } + else + { + frac = min->fraction; + } + lim = frac / 2; + mod = val->fraction % frac; + if (mod < lim) + val->fraction -= mod; /* round down */ + else + val->fraction += frac - mod; /* round up */ +} diff --git a/src/lib/exchange_api_transfers_get.c b/src/lib/exchange_api_transfers_get.c index 1f6e419c9..c558fb42e 100644 --- a/src/lib/exchange_api_transfers_get.c +++ b/src/lib/exchange_api_transfers_get.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2020 Taler Systems SA + Copyright (C) 2014-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 @@ -38,9 +38,9 @@ struct TALER_EXCHANGE_TransfersGetHandle { /** - * The connection to exchange this request handle will use + * The keys of the exchange this request handle will use */ - struct TALER_EXCHANGE_Handle *exchange; + struct TALER_EXCHANGE_Keys *keys; /** * The url for this request. @@ -79,30 +79,39 @@ struct TALER_EXCHANGE_TransfersGetHandle * @return #GNUNET_OK if we are done and all is well, * #GNUNET_SYSERR if the response was bogus */ -static int +static enum GNUNET_GenericReturnValue check_transfers_get_response_ok ( struct TALER_EXCHANGE_TransfersGetHandle *wdh, const json_t *json) { - json_t *details_j; - struct TALER_EXCHANGE_TransferData td; + const json_t *details_j; struct TALER_Amount total_expected; struct TALER_MerchantPublicKeyP merchant_pub; + struct TALER_EXCHANGE_TransfersGetResponse tgr = { + .hr.reply = json, + .hr.http_status = MHD_HTTP_OK + }; + struct TALER_EXCHANGE_TransferData *td + = &tgr.details.ok.td; struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("total", &td.total_amount), - TALER_JSON_spec_amount_any ("wire_fee", &td.wire_fee), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", &merchant_pub), - GNUNET_JSON_spec_fixed_auto ("h_wire", &td.h_wire), - TALER_JSON_spec_absolute_time ("execution_time", &td.execution_time), - GNUNET_JSON_spec_json ("deposits", &details_j), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", &td.exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", &td.exchange_pub), + TALER_JSON_spec_amount_any ("total", + &td->total_amount), + TALER_JSON_spec_amount_any ("wire_fee", + &td->wire_fee), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &merchant_pub), + GNUNET_JSON_spec_fixed_auto ("h_payto", + &td->h_payto), + GNUNET_JSON_spec_timestamp ("execution_time", + &td->execution_time), + GNUNET_JSON_spec_array_const ("deposits", + &details_j), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &td->exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &td->exchange_pub), GNUNET_JSON_spec_end () }; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = json, - .http_status = MHD_HTTP_OK - }; if (GNUNET_OK != GNUNET_JSON_parse (json, @@ -113,32 +122,30 @@ check_transfers_get_response_ok ( return GNUNET_SYSERR; } if (GNUNET_OK != - TALER_amount_set_zero (td.total_amount.currency, + TALER_amount_set_zero (td->total_amount.currency, &total_expected)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); return GNUNET_SYSERR; } if (GNUNET_OK != TALER_EXCHANGE_test_signing_key ( - TALER_EXCHANGE_get_keys (wdh->exchange), - &td.exchange_pub)) + wdh->keys, + &td->exchange_pub)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); return GNUNET_SYSERR; } - td.details_length = json_array_size (details_j); + td->details_length = json_array_size (details_j); { struct GNUNET_HashContext *hash_context; struct TALER_TrackTransferDetails *details; - details = GNUNET_new_array (td.details_length, + details = GNUNET_new_array (td->details_length, struct TALER_TrackTransferDetails); - td.details = details; + td->details = details; hash_context = GNUNET_CRYPTO_hash_context_start (); - for (unsigned int i = 0; i<td.details_length; i++) + for (unsigned int i = 0; i<td->details_length; i++) { struct TALER_TrackTransferDetails *detail = &details[i]; struct json_t *detail_j = json_array_get (details_j, i); @@ -146,21 +153,27 @@ check_transfers_get_response_ok ( GNUNET_JSON_spec_fixed_auto ("h_contract_terms", &detail->h_contract_terms), GNUNET_JSON_spec_fixed_auto ("coin_pub", &detail->coin_pub), - TALER_JSON_spec_amount_any ("deposit_value", &detail->coin_value), - TALER_JSON_spec_amount_any ("deposit_fee", &detail->coin_fee), + TALER_JSON_spec_amount ("deposit_value", + total_expected.currency, + &detail->coin_value), + TALER_JSON_spec_amount ("deposit_fee", + total_expected.currency, + &detail->coin_fee), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount ("refund_total", + total_expected.currency, + &detail->refund_total), + NULL), GNUNET_JSON_spec_end () }; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (td->total_amount.currency, + &detail->refund_total)); if ( (GNUNET_OK != GNUNET_JSON_parse (detail_j, spec_detail, NULL, NULL)) || - (GNUNET_OK != - TALER_amount_cmp_currency (&total_expected, - &detail->coin_value)) || - (GNUNET_OK != - TALER_amount_cmp_currency (&total_expected, - &detail->coin_fee)) || (0 > TALER_amount_add (&total_expected, &total_expected, @@ -172,51 +185,35 @@ check_transfers_get_response_ok ( { GNUNET_break_op (0); GNUNET_CRYPTO_hash_context_abort (hash_context); - GNUNET_JSON_parse_free (spec); GNUNET_free (details); return GNUNET_SYSERR; } /* build up big hash for signature checking later */ - { - struct TALER_WireDepositDetailP dd; - - dd.h_contract_terms = detail->h_contract_terms; - dd.execution_time = GNUNET_TIME_absolute_hton (td.execution_time); - dd.coin_pub = detail->coin_pub; - TALER_amount_hton (&dd.deposit_value, - &detail->coin_value); - TALER_amount_hton (&dd.deposit_fee, - &detail->coin_fee); - GNUNET_CRYPTO_hash_context_read (hash_context, - &dd, - sizeof (dd)); - } + TALER_exchange_online_wire_deposit_append ( + hash_context, + &detail->h_contract_terms, + td->execution_time, + &detail->coin_pub, + &detail->coin_value, + &detail->coin_fee); } /* Check signature */ { - struct TALER_WireDepositDataPS wdp = { - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT), - .purpose.size = htonl (sizeof (wdp)), - .merchant_pub = merchant_pub, - .h_wire = td.h_wire - }; + struct GNUNET_HashCode h_details; - TALER_amount_hton (&wdp.total, - &td.total_amount); - TALER_amount_hton (&wdp.wire_fee, - &td.wire_fee); GNUNET_CRYPTO_hash_context_finish (hash_context, - &wdp.h_details); + &h_details); if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT, - &wdp, - &td.exchange_sig.eddsa_signature, - &td.exchange_pub.eddsa_pub)) + TALER_exchange_online_wire_deposit_verify ( + &td->total_amount, + &td->wire_fee, + &merchant_pub, + &td->h_payto, + &h_details, + &td->exchange_pub, + &td->exchange_sig)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); GNUNET_free (details); return GNUNET_SYSERR; } @@ -225,29 +222,24 @@ check_transfers_get_response_ok ( if (0 > TALER_amount_subtract (&total_expected, &total_expected, - &td.wire_fee)) + &td->wire_fee)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); GNUNET_free (details); return GNUNET_SYSERR; } if (0 != TALER_amount_cmp (&total_expected, - &td.total_amount)) + &td->total_amount)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); GNUNET_free (details); return GNUNET_SYSERR; } wdh->cb (wdh->cb_cls, - &hr, - &td); + &tgr); GNUNET_free (details); } - GNUNET_JSON_parse_free (spec); - TALER_EXCHANGE_transfers_get_cancel (wdh); return GNUNET_OK; } @@ -267,100 +259,85 @@ handle_transfers_get_finished (void *cls, { struct TALER_EXCHANGE_TransfersGetHandle *wdh = cls; const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code + struct TALER_EXCHANGE_TransfersGetResponse tgr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code }; wdh->job = NULL; switch (response_code) { case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + tgr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: if (GNUNET_OK == check_transfers_get_response_ok (wdh, j)) + { + TALER_EXCHANGE_transfers_get_cancel (wdh); return; + } GNUNET_break_op (0); - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - hr.http_status = 0; + tgr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + tgr.hr.http_status = 0; 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); + tgr.hr.ec = TALER_JSON_get_error_code (j); + tgr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_FORBIDDEN: /* 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); + tgr.hr.ec = TALER_JSON_get_error_code (j); + tgr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_NOT_FOUND: /* Exchange does not know about transaction; we should pass the reply to the application */ - hr.ec = TALER_JSON_get_error_code (j); - hr.hint = TALER_JSON_get_error_hint (j); + tgr.hr.ec = TALER_JSON_get_error_code (j); + tgr.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); + tgr.hr.ec = TALER_JSON_get_error_code (j); + tgr.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); + tgr.hr.ec = TALER_JSON_get_error_code (j); + tgr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for transfers get\n", (unsigned int) response_code, - (int) hr.ec); + (int) tgr.hr.ec); break; } wdh->cb (wdh->cb_cls, - &hr, - NULL); + &tgr); TALER_EXCHANGE_transfers_get_cancel (wdh); } -/** - * Query the exchange about which transactions were combined - * to create a wire transfer. - * - * @param exchange exchange to query - * @param wtid raw wire transfer identifier to get information about - * @param cb callback to call - * @param cb_cls closure for @a cb - * @return handle to cancel operation - */ struct TALER_EXCHANGE_TransfersGetHandle * TALER_EXCHANGE_transfers_get ( - struct TALER_EXCHANGE_Handle *exchange, + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, const struct TALER_WireTransferIdentifierRawP *wtid, TALER_EXCHANGE_TransfersGetCallback cb, void *cb_cls) { struct TALER_EXCHANGE_TransfersGetHandle *wdh; - struct GNUNET_CURL_Context *ctx; CURL *eh; char arg_str[sizeof (struct TALER_WireTransferIdentifierRawP) * 2 + 32]; - if (GNUNET_YES != - TEAH_handle_is_ready (exchange)) - { - GNUNET_break (0); - return NULL; - } - wdh = GNUNET_new (struct TALER_EXCHANGE_TransfersGetHandle); - wdh->exchange = exchange; wdh->cb = cb; wdh->cb_cls = cb_cls; @@ -376,11 +353,12 @@ TALER_EXCHANGE_transfers_get ( *end = '\0'; GNUNET_snprintf (arg_str, sizeof (arg_str), - "/transfers/%s", + "transfers/%s", wtid_str); } - wdh->url = TEAH_path_to_url (wdh->exchange, - arg_str); + wdh->url = TALER_url_join (url, + arg_str, + NULL); if (NULL == wdh->url) { GNUNET_free (wdh); @@ -394,7 +372,7 @@ TALER_EXCHANGE_transfers_get ( GNUNET_free (wdh); return NULL; } - ctx = TEAH_handle_to_context (exchange); + wdh->keys = TALER_EXCHANGE_keys_incref (keys); wdh->job = GNUNET_CURL_job_add_with_ct_json (ctx, eh, &handle_transfers_get_finished, @@ -419,6 +397,7 @@ TALER_EXCHANGE_transfers_get_cancel ( wdh->job = NULL; } GNUNET_free (wdh->url); + TALER_EXCHANGE_keys_decref (wdh->keys); GNUNET_free (wdh); } diff --git a/src/lib/exchange_api_wire.c b/src/lib/exchange_api_wire.c deleted file mode 100644 index 5d5a0f4ae..000000000 --- a/src/lib/exchange_api_wire.c +++ /dev/null @@ -1,483 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2020 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_wire.c - * @brief Implementation of the /wire request of the exchange's HTTP API - * @author Christian Grothoff - */ -#include "platform.h" -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include "taler_exchange_service.h" -#include "taler_json_lib.h" -#include "taler_signatures.h" -#include "exchange_api_handle.h" -#include "exchange_api_curl_defaults.h" - - -/** - * @brief A Wire Handle - */ -struct TALER_EXCHANGE_WireHandle -{ - - /** - * 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_WireCallback cb; - - /** - * Closure for @a cb. - */ - void *cb_cls; - -}; - - -/** - * List of wire fees by method. - */ -struct FeeMap -{ - /** - * Next entry in list. - */ - struct FeeMap *next; - - /** - * Wire method this fee structure is for. - */ - char *method; - - /** - * Array of wire fees, also linked list, but allocated - * only once. - */ - struct TALER_EXCHANGE_WireAggregateFees *fee_list; -}; - - -/** - * Frees @a fm. - * - * @param fm memory to release - */ -static void -free_fees (struct FeeMap *fm) -{ - while (NULL != fm) - { - struct FeeMap *fe = fm->next; - - GNUNET_free (fm->fee_list); - GNUNET_free (fm->method); - GNUNET_free (fm); - fm = fe; - } -} - - -/** - * Parse wire @a fees and return map. - * - * @param fees json AggregateTransferFee to parse - * @return NULL on error - */ -static struct FeeMap * -parse_fees (json_t *fees) -{ - struct FeeMap *fm = NULL; - const char *key; - json_t *fee_array; - - json_object_foreach (fees, key, fee_array) { - struct FeeMap *fe = GNUNET_new (struct FeeMap); - unsigned int len; - unsigned int idx; - json_t *fee; - - if (0 == (len = json_array_size (fee_array))) - { - GNUNET_free (fe); - continue; /* skip */ - } - fe->method = GNUNET_strdup (key); - fe->next = fm; - fe->fee_list = GNUNET_new_array (len, - struct TALER_EXCHANGE_WireAggregateFees); - fm = fe; - json_array_foreach (fee_array, idx, fee) - { - struct TALER_EXCHANGE_WireAggregateFees *wa = &fe->fee_list[idx]; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("sig", - &wa->master_sig), - TALER_JSON_spec_amount_any ("wire_fee", - &wa->wire_fee), - TALER_JSON_spec_amount_any ("closing_fee", - &wa->closing_fee), - TALER_JSON_spec_absolute_time ("start_date", - &wa->start_date), - TALER_JSON_spec_absolute_time ("end_date", - &wa->end_date), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (fee, - spec, - NULL, - NULL)) - { - GNUNET_break_op (0); - free_fees (fm); - return NULL; - } - if (idx + 1 < len) - wa->next = &fe->fee_list[idx + 1]; - else - wa->next = NULL; - } - } - return fm; -} - - -/** - * Find fee by @a method. - * - * @param fm map to look in - * @param method key to look for - * @return NULL if fee is not specified in @a fm - */ -static const struct TALER_EXCHANGE_WireAggregateFees * -lookup_fee (const struct FeeMap *fm, - const char *method) -{ - for (; NULL != fm; fm = fm->next) - if (0 == strcasecmp (fm->method, - method)) - return fm->fee_list; - return NULL; -} - - -/** - * Function called when we're done processing the - * HTTP /wire request. - * - * @param cls the `struct TALER_EXCHANGE_WireHandle` - * @param response_code HTTP response code, 0 on error - * @param response parsed JSON result, NULL on error - */ -static void -handle_wire_finished (void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_WireHandle *wh = cls; - const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code - }; - - TALER_LOG_DEBUG ("Checking raw /wire response\n"); - wh->job = NULL; - switch (response_code) - { - case 0: - hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - /* FIXME: Maybe we should only increment when we know it's a timeout? */ - wh->exchange->wire_error_count++; - break; - case MHD_HTTP_OK: - { - json_t *accounts; - json_t *fees; - unsigned int num_accounts; - struct FeeMap *fm; - const struct TALER_EXCHANGE_Keys *key_state; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("accounts", &accounts), - GNUNET_JSON_spec_json ("fees", &fees), - GNUNET_JSON_spec_end () - }; - - wh->exchange->wire_error_count = 0; - - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) - { - /* bogus reply */ - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - if (0 == (num_accounts = json_array_size (accounts))) - { - /* bogus reply */ - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - if (NULL == (fm = parse_fees (fees))) - { - /* bogus reply */ - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - - key_state = TALER_EXCHANGE_get_keys (wh->exchange); - /* parse accounts */ - { - struct TALER_EXCHANGE_WireAccount was[num_accounts]; - - for (unsigned int i = 0; i<num_accounts; i++) - { - struct TALER_EXCHANGE_WireAccount *wa = &was[i]; - json_t *account; - struct GNUNET_JSON_Specification spec_account[] = { - GNUNET_JSON_spec_string ("payto_uri", &wa->payto_uri), - GNUNET_JSON_spec_fixed_auto ("master_sig", &wa->master_sig), - GNUNET_JSON_spec_end () - }; - char *method; - - account = json_array_get (accounts, - i); - if (GNUNET_OK != - TALER_JSON_exchange_wire_signature_check (account, - &key_state->master_pub)) - { - /* bogus reply */ - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_EXCHANGE_WIRE_SIGNATURE_INVALID; - break; - } - if (GNUNET_OK != - GNUNET_JSON_parse (account, - spec_account, - NULL, NULL)) - { - /* bogus reply */ - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - if (NULL == (method = TALER_payto_get_method (wa->payto_uri))) - { - /* bogus reply */ - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - if (NULL == (wa->fees = lookup_fee (fm, - method))) - { - /* bogus reply */ - GNUNET_break_op (0); - hr.http_status = 0; - hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - GNUNET_free (method); - break; - } - GNUNET_free (method); - } /* end 'for all accounts */ - if ( (0 != response_code) && - (NULL != wh->cb) ) - { - wh->cb (wh->cb_cls, - &hr, - num_accounts, - was); - wh->cb = NULL; - } - } /* end of 'parse accounts */ - free_fees (fm); - GNUNET_JSON_parse_free (spec); - } /* end of MHD_HTTP_OK */ - 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_NOT_FOUND: - /* Nothing really to verify, 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_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 */ - if (MHD_HTTP_GATEWAY_TIMEOUT == response_code) - wh->exchange->wire_error_count++; - 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 wire\n", - (unsigned int) response_code, - (int) hr.ec); - break; - } - if (NULL != wh->cb) - wh->cb (wh->cb_cls, - &hr, - 0, - NULL); - TALER_EXCHANGE_wire_cancel (wh); -} - - -/** - * Compute the network timeout for the next request to /wire. - * - * @param exchange the exchange handle - * @returns the timeout in seconds (for use by CURL) - */ -static long -get_wire_timeout_seconds (struct TALER_EXCHANGE_Handle *exchange) -{ - return GNUNET_MIN (60, - 5 + (1L << exchange->wire_error_count)); -} - - -/** - * Obtain information about a exchange's wire instructions. - * A exchange may provide wire instructions for creating - * a reserve. The wire instructions also indicate - * which wire formats merchants may use with the exchange. - * This API is typically used by a wallet for wiring - * funds, and possibly by a merchant to determine - * supported wire formats. - * - * Note that while we return the (main) response verbatim to the - * caller for further processing, we do already verify that the - * response is well-formed (i.e. that signatures included in the - * response are all valid). If the exchange's reply is not well-formed, - * we return an HTTP status code of zero to @a cb. - * - * @param exchange the exchange handle; the exchange must be ready to operate - * @param wire_cb the callback to call when a reply for this request is available - * @param wire_cb_cls closure for the above callback - * @return a handle for this request - */ -struct TALER_EXCHANGE_WireHandle * -TALER_EXCHANGE_wire (struct TALER_EXCHANGE_Handle *exchange, - TALER_EXCHANGE_WireCallback wire_cb, - void *wire_cb_cls) -{ - struct TALER_EXCHANGE_WireHandle *wh; - struct GNUNET_CURL_Context *ctx; - CURL *eh; - - if (GNUNET_YES != - TEAH_handle_is_ready (exchange)) - { - GNUNET_break (0); - return NULL; - } - wh = GNUNET_new (struct TALER_EXCHANGE_WireHandle); - wh->exchange = exchange; - wh->cb = wire_cb; - wh->cb_cls = wire_cb_cls; - wh->url = TEAH_path_to_url (exchange, - "/wire"); - if (NULL == wh->url) - { - GNUNET_free (wh); - return NULL; - } - eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); - GNUNET_break (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_TIMEOUT, - get_wire_timeout_seconds (wh->exchange))); - if (NULL == eh) - { - GNUNET_break (0); - GNUNET_free (wh->url); - GNUNET_free (wh); - return NULL; - } - ctx = TEAH_handle_to_context (exchange); - wh->job = GNUNET_CURL_job_add_with_ct_json (ctx, - eh, - &handle_wire_finished, - wh); - return wh; -} - - -/** - * Cancel a wire information request. This function cannot be used - * on a request handle if a response is already served for it. - * - * @param wh the wire information request handle - */ -void -TALER_EXCHANGE_wire_cancel (struct TALER_EXCHANGE_WireHandle *wh) -{ - if (NULL != wh->job) - { - GNUNET_CURL_job_cancel (wh->job); - wh->job = NULL; - } - GNUNET_free (wh->url); - GNUNET_free (wh); -} - - -/* end of exchange_api_wire.c */ diff --git a/src/lib/exchange_api_withdraw.c b/src/lib/exchange_api_withdraw.c deleted file mode 100644 index b96adacd4..000000000 --- a/src/lib/exchange_api_withdraw.c +++ /dev/null @@ -1,210 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2020 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_withdraw.c - * @brief Implementation of /reserves/$RESERVE_PUB/withdraw requests with blinding/unblinding - * @author Christian Grothoff - */ -#include "platform.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 "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 Withdraw Handle - */ -struct TALER_EXCHANGE_WithdrawHandle -{ - - /** - * The connection to exchange this request handle will use - */ - struct TALER_EXCHANGE_Handle *exchange; - - /** - * Handle for the actual (internal) withdraw operation. - */ - struct TALER_EXCHANGE_Withdraw2Handle *wh2; - - /** - * Function to call with the result. - */ - TALER_EXCHANGE_WithdrawCallback cb; - - /** - * Closure for @a cb. - */ - void *cb_cls; - - /** - * Secrets of the planchet. - */ - struct TALER_PlanchetSecretsP ps; - - /** - * Denomination key we are withdrawing. - */ - struct TALER_EXCHANGE_DenomPublicKey pk; - - /** - * Hash of the public key of the coin we are signing. - */ - struct GNUNET_HashCode c_hash; - -}; - - -/** - * Function called when we're done processing the - * HTTP /reserves/$RESERVE_PUB/withdraw request. - * - * @param cls the `struct TALER_EXCHANGE_WithdrawHandle` - * @param hr HTTP response data - * @param blind_sig blind signature over the coin, NULL on error - */ -static void -handle_reserve_withdraw_finished ( - void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct GNUNET_CRYPTO_RsaSignature *blind_sig) -{ - struct TALER_EXCHANGE_WithdrawHandle *wh = cls; - - wh->wh2 = NULL; - if (MHD_HTTP_OK != hr->http_status) - { - wh->cb (wh->cb_cls, - hr, - NULL); - } - else - { - struct TALER_FreshCoin fc; - - if (GNUNET_OK != - TALER_planchet_to_coin (&wh->pk.key, - blind_sig, - &wh->ps, - &wh->c_hash, - &fc)) - { - struct TALER_EXCHANGE_HttpResponse hrx = { - .reply = hr->reply, - .http_status = 0, - .ec = TALER_EC_EXCHANGE_WITHDRAW_UNBLIND_FAILURE - }; - - wh->cb (wh->cb_cls, - &hrx, - NULL); - } - else - { - wh->cb (wh->cb_cls, - hr, - &fc.sig); - GNUNET_CRYPTO_rsa_signature_free (fc.sig.rsa_signature); - } - - } - TALER_EXCHANGE_withdraw_cancel (wh); -} - - -/** - * Withdraw a coin from the exchange using a /reserve/withdraw request. 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 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 res_cb the callback to call when the final result for this request is available - * @param res_cb_cls closure for the above callback - * @return handle for the operation on success, NULL on error, i.e. - * if the inputs are invalid (i.e. denomination key not with this exchange). - * In this case, the callback is not called. - */ -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_PlanchetSecretsP *ps, - TALER_EXCHANGE_WithdrawCallback res_cb, - void *res_cb_cls) -{ - struct TALER_PlanchetDetail pd; - struct TALER_EXCHANGE_WithdrawHandle *wh; - - wh = GNUNET_new (struct TALER_EXCHANGE_WithdrawHandle); - wh->exchange = exchange; - wh->cb = res_cb; - wh->cb_cls = res_cb_cls; - wh->pk = *pk; - wh->ps = *ps; - if (GNUNET_OK != - TALER_planchet_prepare (&pk->key, - ps, - &wh->c_hash, - &pd)) - { - GNUNET_break (0); - GNUNET_free (wh); - return NULL; - } - wh->pk.key.rsa_public_key - = GNUNET_CRYPTO_rsa_public_key_dup (pk->key.rsa_public_key); - wh->wh2 = TALER_EXCHANGE_withdraw2 (exchange, - &pd, - reserve_priv, - &handle_reserve_withdraw_finished, - wh); - GNUNET_free (pd.coin_ev); - return wh; -} - - -/** - * Cancel a withdraw status request. This function cannot be used - * on a request handle if a response is already served for it. - * - * @param wh the withdraw sign request handle - */ -void -TALER_EXCHANGE_withdraw_cancel (struct TALER_EXCHANGE_WithdrawHandle *wh) -{ - if (NULL != wh->wh2) - { - TALER_EXCHANGE_withdraw2_cancel (wh->wh2); - wh->wh2 = NULL; - } - GNUNET_CRYPTO_rsa_public_key_free (wh->pk.key.rsa_public_key); - GNUNET_free (wh); -} diff --git a/src/lib/exchange_api_withdraw2.c b/src/lib/exchange_api_withdraw2.c deleted file mode 100644 index c8a959688..000000000 --- a/src/lib/exchange_api_withdraw2.c +++ /dev/null @@ -1,491 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2021 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_withdraw2.c - * @brief Implementation of /reserves/$RESERVE_PUB/withdraw requests without blinding/unblinding - * @author Christian Grothoff - */ -#include "platform.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 "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 Withdraw Handle - */ -struct TALER_EXCHANGE_Withdraw2Handle -{ - - /** - * 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_Withdraw2Callback 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; - -}; - - -/** - * We got a 200 OK response for the /reserves/$RESERVE_PUB/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 int -reserve_withdraw_ok (struct TALER_EXCHANGE_Withdraw2Handle *wh, - const json_t *json) -{ - struct GNUNET_CRYPTO_RsaSignature *blind_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_rsa_signature ("ev_sig", - &blind_sig), - GNUNET_JSON_spec_end () - }; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = json, - .http_status = MHD_HTTP_OK - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (json, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - /* signature is valid, return it to the application */ - wh->cb (wh->cb_cls, - &hr, - blind_sig); - /* make sure callback isn't called again after return */ - wh->cb = NULL; - GNUNET_JSON_parse_free (spec); - return GNUNET_OK; -} - - -/** - * We got a 409 CONFLICT response for the /reserves/$RESERVE_PUB/withdraw operation. - * Check the signatures on the 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 int -reserve_withdraw_payment_required ( - struct TALER_EXCHANGE_Withdraw2Handle *wh, - const json_t *json) -{ - struct TALER_Amount balance; - struct TALER_Amount balance_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_ReserveHistory *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_ReserveHistory) - * 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, - &balance_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); - } - - if (0 != - TALER_amount_cmp (&balance_from_history, - &balance)) - { - /* exchange cannot add up balances!? */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - /* 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/withdraw request. - * - * @param cls the `struct TALER_EXCHANGE_WithdrawHandle` - * @param response_code HTTP response code, 0 on error - * @param response parsed JSON result, NULL on error - */ -static void -handle_reserve_withdraw_finished (void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_Withdraw2Handle *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_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_withdraw2_cancel (wh); - 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 */ - 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_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 withdraw\n", - (unsigned int) response_code, - (int) hr.ec); - break; - } - if (NULL != wh->cb) - { - wh->cb (wh->cb_cls, - &hr, - NULL); - wh->cb = NULL; - } - TALER_EXCHANGE_withdraw2_cancel (wh); -} - - -struct TALER_EXCHANGE_Withdraw2Handle * -TALER_EXCHANGE_withdraw2 ( - struct TALER_EXCHANGE_Handle *exchange, - const struct TALER_PlanchetDetail *pd, - const struct TALER_ReservePrivateKeyP *reserve_priv, - TALER_EXCHANGE_Withdraw2Callback res_cb, - void *res_cb_cls) -{ - struct TALER_EXCHANGE_Withdraw2Handle *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]; - - keys = TALER_EXCHANGE_get_keys (exchange); - if (NULL == keys) - { - GNUNET_break (0); - return NULL; - } - dk = TALER_EXCHANGE_get_denomination_key_by_hash (keys, - &pd->denom_pub_hash); - if (NULL == dk) - { - GNUNET_break (0); - return NULL; - } - wh = GNUNET_new (struct TALER_EXCHANGE_Withdraw2Handle); - wh->exchange = exchange; - wh->cb = res_cb; - wh->cb_cls = res_cb_cls; - /* Compute how much we expected to charge to the reserve */ - if (0 > - TALER_amount_add (&wh->requested_amount, - &dk->value, - &dk->fee_withdraw)) - { - /* Overflow here? Very strange, our CPU must be fried... */ - GNUNET_break (0); - GNUNET_free (wh); - return NULL; - } - - 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/withdraw", - pub_str); - } - { - struct TALER_WithdrawRequestPS req = { - .purpose.size = htonl (sizeof (struct TALER_WithdrawRequestPS)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW), - .reserve_pub = wh->reserve_pub, - .h_denomination_pub = pd->denom_pub_hash - }; - - TALER_amount_hton (&req.amount_with_fee, - &wh->requested_amount); - GNUNET_CRYPTO_hash (pd->coin_ev, - pd->coin_ev_size, - &req.h_coin_envelope); - GNUNET_CRYPTO_eddsa_sign (&reserve_priv->eddsa_priv, - &req, - &reserve_sig.eddsa_signature); - } - - { - json_t *withdraw_obj; - - withdraw_obj = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("denom_pub_hash", - &pd->denom_pub_hash), - GNUNET_JSON_pack_data_varsize ("coin_ev", - pd->coin_ev, - pd->coin_ev_size), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &reserve_sig)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Attempting to withdraw from reserve %s\n", - TALER_B2S (&wh->reserve_pub)); - wh->url = TEAH_path_to_url (exchange, - arg_str); - if (NULL == wh->url) - { - json_decref (withdraw_obj); - GNUNET_free (wh); - return NULL; - } - { - CURL *eh; - struct GNUNET_CURL_Context *ctx; - - 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, - withdraw_obj)) ) - { - GNUNET_break (0); - if (NULL != eh) - curl_easy_cleanup (eh); - json_decref (withdraw_obj); - GNUNET_free (wh->url); - GNUNET_free (wh); - return NULL; - } - json_decref (withdraw_obj); - wh->job = GNUNET_CURL_job_add2 (ctx, - eh, - wh->post_ctx.headers, - &handle_reserve_withdraw_finished, - wh); - } - } - return wh; -} - - -void -TALER_EXCHANGE_withdraw2_cancel (struct TALER_EXCHANGE_Withdraw2Handle *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/test_stefan.c b/src/lib/test_stefan.c new file mode 100644 index 000000000..4f7add593 --- /dev/null +++ b/src/lib/test_stefan.c @@ -0,0 +1,206 @@ +/* + 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 + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/test_stefan.c + * @brief test calculations on the STEFAN curve + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "exchange_api_handle.h" + + +/** + * Check if @a a and @a b are numerically close. + * + * @param a an amount + * @param b an amount + * @return true if both values are quite close + */ +static bool +amount_close (const struct TALER_Amount *a, + const struct TALER_Amount *b) +{ + struct TALER_Amount delta; + + switch (TALER_amount_cmp (a, + b)) + { + case -1: /* a < b */ + GNUNET_assert (0 < + TALER_amount_subtract (&delta, + b, + a)); + break; + case 0: + /* perfect */ + return true; + case 1: /* a > b */ + GNUNET_assert (0 < + TALER_amount_subtract (&delta, + a, + b)); + break; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Rounding error is %s\n", + TALER_amount2s (&delta)); + if (delta.value > 0) + { + GNUNET_break (0); + return false; + } + if (delta.fraction > 5000) + { + GNUNET_break (0); + return false; + } + return true; /* let's consider this a rounding error */ +} + + +int +main (int argc, + char **argv) +{ + struct TALER_EXCHANGE_DenomPublicKey dk; + struct TALER_EXCHANGE_Keys keys = { + .denom_keys = &dk, + .num_denom_keys = 1 + }; + struct TALER_Amount brut; + struct TALER_Amount net; + + (void) argc; + (void) argv; + GNUNET_log_setup ("test-stefan", + "INFO", + NULL); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("MAGIC:0.00001", + &dk.value)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("MAGIC:1", + &keys.stefan_abs)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("MAGIC:0.13", + &keys.stefan_log)); + keys.stefan_lin = 1.15; + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("MAGIC:4", + &brut)); + GNUNET_log_skip (1, + GNUNET_NO); + GNUNET_assert (GNUNET_SYSERR == + TALER_EXCHANGE_keys_stefan_b2n (&keys, + &brut, + &net)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("MAGIC:4", + &net)); + GNUNET_log_skip (1, + GNUNET_NO); + GNUNET_assert (GNUNET_SYSERR == + TALER_EXCHANGE_keys_stefan_n2b (&keys, + &net, + &brut)); + keys.stefan_lin = 1.0; + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("MAGIC:4", + &brut)); + GNUNET_log_skip (1, + GNUNET_NO); + GNUNET_assert (GNUNET_SYSERR == + TALER_EXCHANGE_keys_stefan_b2n (&keys, + &brut, + &net)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("MAGIC:4", + &net)); + GNUNET_log_skip (1, + GNUNET_NO); + GNUNET_assert (GNUNET_SYSERR == + TALER_EXCHANGE_keys_stefan_n2b (&keys, + &net, + &brut)); + GNUNET_assert (0 == GNUNET_get_log_skip ()); + keys.stefan_lin = 0.1; + + /* try various values for lin and log STEFAN values */ + for (unsigned int li = 1; li < 13; li += 1) + { + keys.stefan_lin = 1.0 * li / 100.0; + + for (unsigned int lx = 1; lx < 100; lx += 1) + { + keys.stefan_log.fraction = lx * TALER_AMOUNT_FRAC_BASE / 100; + + /* Check brutto-to-netto is stable */ + for (unsigned int i = 0; i<10; i++) + { + struct TALER_Amount rval; + + brut.value = i; + brut.fraction = i * TALER_AMOUNT_FRAC_BASE / 10; + GNUNET_assert (GNUNET_SYSERR != + TALER_EXCHANGE_keys_stefan_b2n (&keys, + &brut, + &net)); + GNUNET_assert (GNUNET_SYSERR != + TALER_EXCHANGE_keys_stefan_n2b (&keys, + &net, + &rval)); + if (TALER_amount_is_zero (&net)) + GNUNET_assert (TALER_amount_is_zero (&rval)); + else + { + GNUNET_assert (amount_close (&brut, + &rval)); + TALER_EXCHANGE_keys_stefan_round (&keys, + &rval); + GNUNET_assert (amount_close (&brut, + &rval)); + } + } + + /* Check netto-to-brutto is stable */ + for (unsigned int i = 0; i<10; i++) + { + struct TALER_Amount rval; + + net.value = i; + net.fraction = i * TALER_AMOUNT_FRAC_BASE / 10; + GNUNET_assert (GNUNET_SYSERR != + TALER_EXCHANGE_keys_stefan_n2b (&keys, + &net, + &brut)); + GNUNET_assert (GNUNET_SYSERR != + TALER_EXCHANGE_keys_stefan_b2n (&keys, + &brut, + &rval)); + GNUNET_assert (amount_close (&net, + &rval)); + TALER_EXCHANGE_keys_stefan_round (&keys, + &rval); + GNUNET_assert (amount_close (&net, + &rval)); + } + } + } + return 0; +} |