diff options
Diffstat (limited to 'src/exchange')
116 files changed, 24062 insertions, 6310 deletions
diff --git a/src/exchange/.gitignore b/src/exchange/.gitignore index 5818f1717..bcfdb7e82 100644 --- a/src/exchange/.gitignore +++ b/src/exchange/.gitignore @@ -9,3 +9,5 @@ test_taler_exchange_wirewatch-postgres test_taler_exchange_httpd_home/.config/taler/account-1.json taler-exchange-closer taler-exchange-transfer +taler-exchange-router +taler-exchange-expire diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index e7688f735..1c0c2c684 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -15,11 +15,16 @@ pkgcfg_DATA = \ exchange.conf # Programs +bin_SCRIPTS = \ + taler-exchange-kyc-aml-pep-trigger.sh bin_PROGRAMS = \ taler-exchange-aggregator \ taler-exchange-closer \ + taler-exchange-drain \ + taler-exchange-expire \ taler-exchange-httpd \ + taler-exchange-router \ taler-exchange-transfer \ taler-exchange-wirewatch @@ -27,6 +32,7 @@ taler_exchange_aggregator_SOURCES = \ taler-exchange-aggregator.c taler_exchange_aggregator_LDADD = \ $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/kyclogic/libtalerkyclogic.la \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/util/libtalerutil.la \ $(top_builddir)/src/bank-lib/libtalerbank.la \ @@ -50,9 +56,35 @@ taler_exchange_closer_LDADD = \ -lgnunetutil \ $(XLIB) -taler_exchange_wirewatch_SOURCES = \ - taler-exchange-wirewatch.c -taler_exchange_wirewatch_LDADD = \ +taler_exchange_drain_SOURCES = \ + taler-exchange-drain.c +taler_exchange_drain_LDADD = \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/bank-lib/libtalerbank.la \ + $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + -ljansson \ + -lgnunetcurl \ + -lgnunetutil \ + $(XLIB) + +taler_exchange_expire_SOURCES = \ + taler-exchange-expire.c +taler_exchange_expire_LDADD = \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/bank-lib/libtalerbank.la \ + $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + -ljansson \ + -lgnunetcurl \ + -lgnunetutil \ + $(XLIB) + +taler_exchange_router_SOURCES = \ + taler-exchange-router.c +taler_exchange_router_LDADD = \ $(LIBGCRYPT_LIBS) \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/util/libtalerutil.la \ @@ -76,23 +108,54 @@ taler_exchange_transfer_LDADD = \ -lgnunetutil \ $(XLIB) +taler_exchange_wirewatch_SOURCES = \ + taler-exchange-wirewatch.c +taler_exchange_wirewatch_LDADD = \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/bank-lib/libtalerbank.la \ + $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + -ljansson \ + -lgnunetcurl \ + -lgnunetutil \ + $(XLIB) + + taler_exchange_httpd_SOURCES = \ taler-exchange-httpd.c taler-exchange-httpd.h \ + taler-exchange-httpd_age-withdraw.c taler-exchange-httpd_age-withdraw.h \ + taler-exchange-httpd_age-withdraw_reveal.c taler-exchange-httpd_age-withdraw_reveal.h \ taler-exchange-httpd_auditors.c taler-exchange-httpd_auditors.h \ + taler-exchange-httpd_aml-decision.c taler-exchange-httpd_aml-decision.h \ + taler-exchange-httpd_aml-decision-get.c \ + taler-exchange-httpd_aml-decisions-get.c \ + taler-exchange-httpd_batch-deposit.c taler-exchange-httpd_batch-deposit.h \ + taler-exchange-httpd_batch-withdraw.c taler-exchange-httpd_batch-withdraw.h \ + taler-exchange-httpd_coins_get.c taler-exchange-httpd_coins_get.h \ + taler-exchange-httpd_common_deposit.c taler-exchange-httpd_common_deposit.h \ + taler-exchange-httpd_common_kyc.c taler-exchange-httpd_common_kyc.h \ + taler-exchange-httpd_config.c taler-exchange-httpd_config.h \ + taler-exchange-httpd_contract.c taler-exchange-httpd_contract.h \ + taler-exchange-httpd_csr.c taler-exchange-httpd_csr.h \ taler-exchange-httpd_db.c taler-exchange-httpd_db.h \ - taler-exchange-httpd_deposit.c taler-exchange-httpd_deposit.h \ taler-exchange-httpd_deposits_get.c taler-exchange-httpd_deposits_get.h \ taler-exchange-httpd_extensions.c taler-exchange-httpd_extensions.h \ taler-exchange-httpd_keys.c taler-exchange-httpd_keys.h \ taler-exchange-httpd_kyc-check.c taler-exchange-httpd_kyc-check.h \ taler-exchange-httpd_kyc-proof.c taler-exchange-httpd_kyc-proof.h \ taler-exchange-httpd_kyc-wallet.c taler-exchange-httpd_kyc-wallet.h \ + taler-exchange-httpd_kyc-webhook.c taler-exchange-httpd_kyc-webhook.h \ taler-exchange-httpd_link.c taler-exchange-httpd_link.h \ taler-exchange-httpd_management.h \ + taler-exchange-httpd_management_aml-officers.c \ taler-exchange-httpd_management_auditors.c \ taler-exchange-httpd_management_auditors_AP_disable.c \ taler-exchange-httpd_management_denominations_HDP_revoke.c \ + taler-exchange-httpd_management_drain.c \ taler-exchange-httpd_management_extensions.c \ + taler-exchange-httpd_management_global_fees.c \ + taler-exchange-httpd_management_partners.c \ taler-exchange-httpd_management_post_keys.c \ taler-exchange-httpd_management_signkey_EP_revoke.c \ taler-exchange-httpd_management_wire_enable.c \ @@ -101,16 +164,26 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd_melt.c taler-exchange-httpd_melt.h \ taler-exchange-httpd_metrics.c taler-exchange-httpd_metrics.h \ taler-exchange-httpd_mhd.c taler-exchange-httpd_mhd.h \ + taler-exchange-httpd_purses_create.c taler-exchange-httpd_purses_create.h \ + taler-exchange-httpd_purses_deposit.c taler-exchange-httpd_purses_deposit.h \ + taler-exchange-httpd_purses_delete.c taler-exchange-httpd_purses_delete.h \ + taler-exchange-httpd_purses_get.c taler-exchange-httpd_purses_get.h \ + taler-exchange-httpd_purses_merge.c taler-exchange-httpd_purses_merge.h \ taler-exchange-httpd_recoup.c taler-exchange-httpd_recoup.h \ taler-exchange-httpd_recoup-refresh.c taler-exchange-httpd_recoup-refresh.h \ taler-exchange-httpd_refreshes_reveal.c taler-exchange-httpd_refreshes_reveal.h \ taler-exchange-httpd_refund.c taler-exchange-httpd_refund.h \ + taler-exchange-httpd_reserves_attest.c taler-exchange-httpd_reserves_attest.h \ + taler-exchange-httpd_reserves_close.c taler-exchange-httpd_reserves_close.h \ taler-exchange-httpd_reserves_get.c taler-exchange-httpd_reserves_get.h \ + taler-exchange-httpd_reserves_get_attest.c taler-exchange-httpd_reserves_get_attest.h \ + taler-exchange-httpd_reserves_history.c taler-exchange-httpd_reserves_history.h \ + taler-exchange-httpd_reserves_open.c taler-exchange-httpd_reserves_open.h \ + taler-exchange-httpd_reserves_purse.c taler-exchange-httpd_reserves_purse.h \ taler-exchange-httpd_responses.c taler-exchange-httpd_responses.h \ + taler-exchange-httpd_spa.c taler-exchange-httpd_spa.h \ taler-exchange-httpd_terms.c taler-exchange-httpd_terms.h \ - taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h \ - taler-exchange-httpd_wire.c taler-exchange-httpd_wire.h \ - taler-exchange-httpd_withdraw.c taler-exchange-httpd_withdraw.h + taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h taler_exchange_httpd_LDADD = \ $(LIBGCRYPT_LIBS) \ @@ -118,7 +191,10 @@ taler_exchange_httpd_LDADD = \ $(top_builddir)/src/mhd/libtalermhd.la \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + $(top_builddir)/src/templating/libtalertemplating.la \ + $(top_builddir)/src/kyclogic/libtalerkyclogic.la \ $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/extensions/libtalerextensions.la \ -lmicrohttpd \ -lgnunetcurl \ -lgnunetutil \ @@ -139,7 +215,6 @@ check_SCRIPTS += \ test_taler_exchange_httpd_afl.sh endif -.NOTPARALLEL: TESTS = \ $(check_SCRIPTS) @@ -152,4 +227,5 @@ EXTRA_DIST = \ test_taler_exchange_httpd.get \ test_taler_exchange_httpd.post \ exchange.conf \ + $(bin_SCRIPTS) \ $(check_SCRIPTS) diff --git a/src/exchange/exchange.conf b/src/exchange/exchange.conf index 92de5e31c..ce471a292 100644 --- a/src/exchange/exchange.conf +++ b/src/exchange/exchange.conf @@ -6,6 +6,33 @@ # This must be adjusted to your actual installation. # MASTER_PUBLIC_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG +# Must be set to the threshold above which transactions +# are flagged for AML review. +# AML_THRESHOLD = + +# How many digits does the currency use by default on displays. +# Hint provided to wallets. Should be 2 for EUR/USD/CHF, +# and 0 for JPY. Default is 2 as that is most common. +# Maximum value is 8. Note that this is the number of +# fractions shown in the wallet by default, it is still +# possible to configure denominations with more digits +# and those will then be rendered using 'tiny' fraction +# capitals (like at gas stations) when present. +CURRENCY_FRACTION_DIGITS = 2 + +# Specifies a program (binary) to run on KYC attribute data to decide +# whether we should immediately flag an account for AML review. +# The KYC attribute data will be passed on standard-input. +# Return non-zero to trigger AML review of the new user. +KYC_AML_TRIGGER = true + +# Attribute encryption key for storing attributes encrypted +# in the database. Should be a high-entropy nonce. +ATTRIBUTE_ENCRYPTION_KEY = SET_ME_PLEASE + +# Set to NO to disable rewards. +ENABLE_REWARDS = YES + # How long do we allow /keys to be cached at most? The actual # limit is the minimum of this value and the first expected # significant change in /keys based on the expiration times. @@ -15,7 +42,7 @@ MAX_KEYS_CACHING = forever # After how many requests should the exchange auto-restart # (to address potential issues with memory fragmentation)? # If this option is not specified, auto-restarting is disabled. -# MAX_REQUESTS = 10000000 +# MAX_REQUESTS = 100000 # How to access our database DB = postgres @@ -40,13 +67,26 @@ PORT = 8081 # transfers to enable tracking. BASE_URL = http://localhost:8081/ -# Maximum number of requests this process should handle before -# committing suicide. -# MAX_REQUESTS = - # How long should the aggregator sleep if it has nothing to do? AGGREGATOR_IDLE_SLEEP_INTERVAL = 60 s +# What type of asset is the exchange managing? Used to adjust +# the user-interface of the wallet. +# Possibilities include: "fiat", "regional" and "crypto". +# In the future (and already permitted but not yet supported by wallets) +# we also expect to have "stock" and "future" (and more). +# Default is "fiat". +ASSET_TYPE = "fiat" + +# FIXME: document! +ROUTER_IDLE_SLEEP_INTERVAL = 60 s + +# How big is an individual shard to be processed +# by taler-exchange-expire (in time). It may take +# this much time for an expired purse to be really +# cleaned up and the coins refunded. +EXPIRE_SHARD_SIZE = 60 s + # How long should the transfer tool # sleep if it has nothing to do? TRANSFER_IDLE_SLEEP_INTERVAL = 60 s @@ -66,6 +106,17 @@ CLOSER_IDLE_SLEEP_INTERVAL = 60 s # aggregation logic will break badly! AGGREGATOR_SHARD_SIZE = 2147483648 +# Values of 0 or above 2^31 disable sharding, which +# is a sane default for most use-cases. +# When changing this value, you MUST stop all +# aggregators and manually run +# +# $ taler-exchange-dbinit -s +# +# against the exchange's database. Otherwise, the +# aggregation logic will break badly! +ROUTER_SHARD_SIZE = 2147483648 + # How long should wirewatch sleep if it has nothing to do? # (Set very aggressively here for the demonstrators to be # super fast.) @@ -75,42 +126,13 @@ WIREWATCH_IDLE_SLEEP_INTERVAL = 1 s SIGNKEY_LEGAL_DURATION = 2 years # Directory with our terms of service. -TERMS_DIR = $DATADIR/exchange/tos/ +TERMS_DIR = $TALER_DATA_HOME/terms/ # Etag / filename for the terms of service. -TERMS_ETAG = 0 +TERMS_ETAG = exchange-tos-v0 # Directory with our privacy policy. -PRIVACY_DIR = $DATADIR/exchange/pp/ +PRIVACY_DIR = $TALER_DATA_HOME/terms/ # Etag / filename for the privacy policy. -PRIVACY_ETAG = 0 - -# Set to NONE to disable KYC checks. -# Set to "OAUTH2" to use OAuth 2.0 for KYC authorization. -KYC_MODE = NONE - -# Balance threshold above which wallets are told -# to undergo a KYC check at the exchange. Optional, -# if not given there is no limit. -# KYC_WALLET_BALANCE_LIMIT = CURRENCY:150 -# -# KYC_WITHDRAW_PERIOD = 1 month - -[exchange-kyc-oauth2] - -# URL of the OAuth endpoint for KYC checks -# KYC_OAUTH2_URL = - -# URL of the "information" endpoint for KYC checks -# KYC_INFO_URL = - -# KYC Oauth client ID. -# KYC_OAUTH2_CLIENT_ID = - -# KYC Client secret used to obtain access tokens. -# KYC_OAUTH2_CLIENT_SECRET = - -# Where to redirect clients after successful -# authorization? -# KYC_OAUTH2_POST_URL = https://bank.com/ +PRIVACY_ETAG = exchange-pp-v0 diff --git a/src/exchange/taler-exchange-aggregator.c b/src/exchange/taler-exchange-aggregator.c index 32b069f90..691d65ae3 100644 --- a/src/exchange/taler-exchange-aggregator.c +++ b/src/exchange/taler-exchange-aggregator.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2016-2021 Taler Systems SA + Copyright (C) 2016-2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -26,7 +26,9 @@ #include "taler_exchangedb_lib.h" #include "taler_exchangedb_plugin.h" #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_bank_service.h" +#include "taler_dbevents.h" /** @@ -43,7 +45,13 @@ struct AggregationUnit struct TALER_MerchantPublicKeyP merchant_pub; /** - * Total amount to be transferred, before subtraction of @e wire_fee and rounding down. + * Transient amount already found aggregated, + * set only if @e have_transient is true. + */ + struct TALER_Amount trans; + + /** + * Total amount to be transferred, before subtraction of @e fees.wire and rounding down. */ struct TALER_Amount total_amount; @@ -55,7 +63,7 @@ struct AggregationUnit /** * Wire fee we charge for @e wp at @e execution_time. */ - struct TALER_Amount wire_fee; + struct TALER_WireFeeSet fees; /** * Wire transfer identifier we use. @@ -63,11 +71,6 @@ struct AggregationUnit struct TALER_WireTransferIdentifierRawP wtid; /** - * Row ID of the transaction that started it all. - */ - uint64_t row_id; - - /** * The current time (which triggered the aggregation and * defines the wire fee). */ @@ -81,7 +84,7 @@ struct AggregationUnit /** * Selected wire target for the aggregation. */ - uint64_t wire_target; + struct TALER_PaytoHashP h_payto; /** * Exchange wire account to be used for the preparation and @@ -90,20 +93,24 @@ struct AggregationUnit const struct TALER_EXCHANGEDB_AccountInfo *wa; /** - * Array of row_ids from the aggregation. + * Row in KYC table for legitimization requirements + * that are pending for this aggregation, or 0 if none. */ - uint64_t additional_rows[TALER_EXCHANGEDB_MATCHING_DEPOSITS_LIMIT]; + uint64_t requirement_row; /** - * Offset specifying how many @e additional_rows are in use. + * Set to #GNUNET_OK during transient checking + * while everything is OK. Otherwise see return + * value of #do_aggregate(). */ - unsigned int rows_offset; + enum GNUNET_GenericReturnValue ret; /** - * Set to true if we encountered a refund during #refund_by_coin_cb. - * Used to wave the deposit fee. + * Do we have an entry in the transient table for + * this aggregation? */ - bool have_refund; + bool have_transient; + }; @@ -143,6 +150,12 @@ struct Shard static struct TALER_Amount currency_round_unit; /** + * What is the largest amount we transfer before triggering + * an AML check? + */ +static struct TALER_Amount aml_threshold; + +/** * What is the base URL of this exchange? Used in the * wire transfer subjects so that merchants and governments * can ask for the list of aggregated deposits. @@ -171,7 +184,6 @@ static struct TALER_EXCHANGEDB_Plugin *db_plugin; */ static struct GNUNET_SCHEDULER_Task *task; - /** * How long should we sleep when idle before trying to find more work? */ @@ -199,19 +211,19 @@ static int test_mode; * Main work function that queries the DB and aggregates transactions * into larger wire transfers. * - * @param cls NULL + * @param cls a `struct Shard *` */ static void run_aggregation (void *cls); /** - * Select a shard to work on. + * Work on transactions unlocked by KYC. * * @param cls NULL */ static void -run_shard (void *cls); +drain_kyc_alerts (void *cls); /** @@ -246,6 +258,7 @@ shutdown_task (void *cls) GNUNET_SCHEDULER_cancel (task); task = NULL; } + TALER_KYCLOGIC_kyc_done (); TALER_EXCHANGEDB_plugin_unload (db_plugin); db_plugin = NULL; TALER_EXCHANGEDB_unload_accounts (); @@ -254,12 +267,12 @@ shutdown_task (void *cls) /** - * Parse the configuration for wirewatch. + * Parse the configuration for aggregator. * * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue -parse_wirewatch_config (void) +parse_aggregator_config (void) { if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, @@ -288,11 +301,20 @@ parse_wirewatch_config (void) "taler", "CURRENCY_ROUND_UNIT", ¤cy_round_unit)) || - ( (0 != currency_round_unit.fraction) && - (0 != currency_round_unit.value) ) ) + (TALER_amount_is_zero (¤cy_round_unit)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Need non-zero amount in section `taler' under `CURRENCY_ROUND_UNIT'\n"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_config_get_amount (cfg, + "exchange", + "AML_THRESHOLD", + &aml_threshold)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Need non-zero value in section `TALER' under `CURRENCY_ROUND_UNIT'\n"); + "Need amount in section `exchange' under `AML_THRESHOLD'\n"); return GNUNET_SYSERR; } @@ -318,28 +340,235 @@ parse_wirewatch_config (void) /** - * Callback invoked with information about refunds applicable - * to a particular coin. Subtract refunded amount(s) from - * the aggregation unit's total amount. + * Perform a database commit. If it fails, print a warning. + * + * @return status of commit + */ +static enum GNUNET_DB_QueryStatus +commit_or_warn (void) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->commit (db_plugin->cls); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return qs; + GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? GNUNET_ERROR_TYPE_INFO + : GNUNET_ERROR_TYPE_ERROR, + "Failed to commit database transaction!\n"); + return qs; +} + + +/** + * Release lock on shard @a s in the database. + * On error, terminates this process. * - * @param cls closure with a `struct AggregationUnit *` - * @param amount_with_fee what was the refunded amount with the fee - * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + * @param[in] s shard to free (and memory to release) + */ +static void +release_shard (struct Shard *s) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->release_revolving_shard ( + db_plugin->cls, + "aggregator", + s->shard_start, + s->shard_end); + GNUNET_free (s); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Strange, but let's just continue */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* normal case */ + break; + } +} + + +/** + * Trigger the wire transfer for the @a au_active + * and delete the record of the aggregation. + * + * @param au_active information about the aggregation + */ +static enum GNUNET_DB_QueryStatus +trigger_wire_transfer (const struct AggregationUnit *au_active) +{ + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Preparing wire transfer of %s to %s\n", + TALER_amount2s (&au_active->final_amount), + TALER_B2S (&au_active->merchant_pub)); + { + void *buf; + size_t buf_size; + + TALER_BANK_prepare_transfer (au_active->payto_uri, + &au_active->final_amount, + exchange_base_url, + &au_active->wtid, + &buf, + &buf_size); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Storing %u bytes of wire prepare data\n", + (unsigned int) buf_size); + /* Commit our intention to execute the wire transfer! */ + qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, + au_active->wa->method, + buf, + buf_size); + GNUNET_free (buf); + } + /* Commit the WTID data to 'wire_out' */ + if (qs >= 0) + qs = db_plugin->store_wire_transfer_out (db_plugin->cls, + au_active->execution_time, + &au_active->wtid, + &au_active->h_payto, + au_active->wa->section_name, + &au_active->final_amount); + + if ( (qs >= 0) && + au_active->have_transient) + qs = db_plugin->delete_aggregation_transient (db_plugin->cls, + &au_active->h_payto, + &au_active->wtid); + return qs; +} + + +/** + * Callback to return all applicable amounts for the KYC + * decision to @ a cb. + * + * @param cls a `struct AggregationUnit *` + * @param limit time limit for the iteration + * @param cb function to call with the amounts + * @param cb_cls closure for @a cb + */ +static void +return_relevant_amounts (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + const struct AggregationUnit *au_active = cls; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning amount %s in KYC check\n", + TALER_amount2s (&au_active->total_amount)); + if (GNUNET_OK != + cb (cb_cls, + &au_active->total_amount, + GNUNET_TIME_absolute_get ())) + return; + qs = db_plugin->select_aggregation_amounts_for_kyc_check ( + db_plugin->cls, + &au_active->h_payto, + limit, + cb, + cb_cls); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to select aggregation amounts for KYC limit check!\n"); + } +} + + +/** + * Test if KYC is required for a transfer to @a h_payto. + * + * @param[in,out] au_active aggregation unit to check for + * @return true if KYC checks are satisfied + */ +static bool +kyc_satisfied (struct AggregationUnit *au_active) +{ + char *requirement; + enum GNUNET_DB_QueryStatus qs; + + if (kyc_off) + return true; + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, + &au_active->h_payto, + db_plugin->select_satisfied_kyc_processes, + db_plugin->cls, + &return_relevant_amounts, + (void *) au_active, + &requirement); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; + } + if (NULL == requirement) + return true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC requirement for %s is %s\n", + TALER_amount2s (&au_active->total_amount), + requirement); + qs = db_plugin->insert_kyc_requirement_for_account ( + db_plugin->cls, + requirement, + &au_active->h_payto, + NULL, /* not a reserve */ + &au_active->requirement_row); + if (qs < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to persist KYC requirement `%s' in DB!\n", + requirement); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Legitimization process %llu started\n", + (unsigned long long) au_active->requirement_row); + } + GNUNET_free (requirement); + return false; +} + + +/** + * Function called on each @a amount that was found to + * be relevant for an AML check. + * + * @param cls closure with the `struct TALER_Amount *` where we store the sum + * @param amount encountered transaction amount + * @param date when was the amount encountered + * @return #GNUNET_OK to continue to iterate, + * #GNUNET_NO to abort iteration + * #GNUNET_SYSERR on internal error (also abort itaration) */ static enum GNUNET_GenericReturnValue -refund_by_coin_cb (void *cls, - const struct TALER_Amount *amount_with_fee) +sum_for_aml ( + void *cls, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Absolute date) { - struct AggregationUnit *aux = cls; + struct TALER_Amount *sum = cls; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Aggregator subtracts applicable refund of amount %s\n", - TALER_amount2s (amount_with_fee)); - aux->have_refund = true; + (void) date; if (0 > - TALER_amount_subtract (&aux->total_amount, - &aux->total_amount, - amount_with_fee)) + TALER_amount_add (sum, + sum, + amount)) { GNUNET_break (0); return GNUNET_SYSERR; @@ -349,117 +578,117 @@ refund_by_coin_cb (void *cls, /** - * Function called with details about deposits that have been made, - * with the goal of executing the corresponding wire transaction. + * Test if AML is required for a transfer to @a h_payto. * - * @param cls a `struct AggregationUnit` - * @param row_id identifies database entry - * @param merchant_pub public key of the merchant - * @param coin_pub public key of the coin - * @param amount_with_fee amount that was deposited including fee - * @param deposit_fee amount the exchange gets to keep as transaction fees - * @param h_contract_terms hash of the proposal data known to merchant and customer - * @param wire_target target account for the wire transfer - * @param payto_uri URI of the target account - * @return transaction status code, #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT to continue to iterate + * @param[in,out] au_active aggregation unit to check for + * @return true if AML checks are satisfied */ -static enum GNUNET_DB_QueryStatus -deposit_cb (void *cls, - uint64_t row_id, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_PrivateContractHash *h_contract_terms, - uint64_t wire_target, - const char *payto_uri) +static bool +aml_satisfied (struct AggregationUnit *au_active) { - struct AggregationUnit *au = cls; enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount total; + struct TALER_Amount threshold; + enum TALER_AmlDecisionState decision; + struct TALER_EXCHANGEDB_KycStatus kyc; - au->merchant_pub = *merchant_pub; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Aggregator processing payment %s with amount %s\n", - TALER_B2S (coin_pub), - TALER_amount2s (amount_with_fee)); - au->row_id = row_id; - au->total_amount = *amount_with_fee; - au->have_refund = false; - qs = db_plugin->select_refunds_by_coin (db_plugin->cls, - coin_pub, - &au->merchant_pub, - h_contract_terms, - &refund_by_coin_cb, - au); - if (0 > qs) + total = au_active->final_amount; + qs = db_plugin->select_aggregation_amounts_for_kyc_check ( + db_plugin->cls, + &au_active->h_payto, + GNUNET_TIME_absolute_subtract (GNUNET_TIME_absolute_get (), + GNUNET_TIME_UNIT_MONTHS), + &sum_for_aml, + &total); + if (qs < 0) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; + return false; } - if (! au->have_refund) + qs = db_plugin->select_aml_threshold (db_plugin->cls, + &au_active->h_payto, + &decision, + &kyc, + &threshold); + if (qs < 0) { - struct TALER_Amount ntotal; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Non-refunded transaction, subtracting deposit fee %s\n", - TALER_amount2s (deposit_fee)); - if (0 > - TALER_amount_subtract (&ntotal, - amount_with_fee, - deposit_fee)) + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + threshold = aml_threshold; /* use default */ + decision = TALER_AML_NORMAL; + } + switch (decision) + { + case TALER_AML_NORMAL: + if (0 >= TALER_amount_cmp (&total, + &threshold)) { - /* This should never happen, issue a warning, but continue processing - with an amount of zero, least we hang here for good. */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Fatally malformed record at row %llu over %s (deposit fee exceeds deposited value)\n", - (unsigned long long) row_id, - TALER_amount2s (amount_with_fee)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (au->total_amount.currency, - &au->total_amount)); + /* total <= threshold, do nothing */ + return true; } - else + qs = db_plugin->trigger_aml_process (db_plugin->cls, + &au_active->h_payto, + &total); + if (qs < 0) { - au->total_amount = ntotal; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; } + return false; + case TALER_AML_PENDING: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "AML already pending, doing nothing\n"); + return false; + case TALER_AML_FROZEN: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Account frozen, doing nothing\n"); + return false; } + GNUNET_assert (0); + return false; +} - GNUNET_assert (NULL == au->payto_uri); - au->payto_uri = GNUNET_strdup (payto_uri); - au->wire_target = wire_target; - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &au->wtid, - sizeof (au->wtid)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting aggregation under H(WTID)=%s, starting amount %s at %llu\n", - TALER_B2S (&au->wtid), - TALER_amount2s (amount_with_fee), - (unsigned long long) row_id); - au->wa = TALER_EXCHANGEDB_find_account_by_payto_uri (payto_uri); + +/** + * Perform the main aggregation work for @a au. Expects to be in + * a working transaction, which the caller must also ultimately commit + * (or rollback) depending on our return value. + * + * @param[in,out] au aggregation unit to work on + * @return #GNUNET_OK if aggregation succeeded, + * #GNUNET_NO to rollback and try again (serialization issue) + * #GNUNET_SYSERR hard error, terminate aggregator process + */ +static enum GNUNET_GenericReturnValue +do_aggregate (struct AggregationUnit *au) +{ + enum GNUNET_DB_QueryStatus qs; + + au->wa = TALER_EXCHANGEDB_find_account_by_payto_uri ( + au->payto_uri); if (NULL == au->wa) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No exchange account configured for `%s', please fix your setup to continue!\n", - payto_uri); - return GNUNET_DB_STATUS_HARD_ERROR; + au->payto_uri); + global_ret = EXIT_FAILURE; + return GNUNET_SYSERR; } - /* make sure we have current fees */ - au->execution_time = GNUNET_TIME_timestamp_get (); { - struct TALER_Amount closing_fee; struct GNUNET_TIME_Timestamp start_date; struct GNUNET_TIME_Timestamp end_date; struct TALER_MasterSignatureP master_sig; - enum GNUNET_DB_QueryStatus qs; qs = db_plugin->get_wire_fee (db_plugin->cls, au->wa->method, au->execution_time, &start_date, &end_date, - &au->wire_fee, - &closing_fee, + &au->fees, &master_sig); if (0 >= qs) { @@ -467,234 +696,178 @@ deposit_cb (void *cls, "Could not get wire fees for %s at %s. Aborting run.\n", au->wa->method, GNUNET_TIME_timestamp2s (au->execution_time)); - return GNUNET_DB_STATUS_HARD_ERROR; + global_ret = EXIT_FAILURE; + return GNUNET_SYSERR; } } + /* Now try to find other deposits to aggregate */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Aggregator starts aggregation for deposit %llu to %s with wire fee %s\n", - (unsigned long long) row_id, - TALER_B2S (&au->wtid), - TALER_amount2s (&au->wire_fee)); - qs = db_plugin->insert_aggregation_tracking (db_plugin->cls, - &au->wtid, - row_id); - if (qs <= 0) + "Found ready deposit for %s, aggregating by target %s\n", + TALER_B2S (&au->merchant_pub), + au->payto_uri); + qs = db_plugin->select_aggregation_transient (db_plugin->cls, + &au->h_payto, + &au->merchant_pub, + au->wa->section_name, + &au->wtid, + &au->trans); + switch (qs) { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to lookup transient aggregates!\n"); + global_ret = EXIT_FAILURE; + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* serializiability issue, try again */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Serialization issue, trying again later!\n"); + return GNUNET_NO; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &au->wtid, + sizeof (au->wtid)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No transient aggregation found, starting %s\n", + TALER_B2S (&au->wtid)); + au->have_transient = false; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + au->have_transient = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Transient aggregation found, resuming %s\n", + TALER_B2S (&au->wtid)); + break; } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Aggregator marks deposit %llu as done\n", - (unsigned long long) row_id); - qs = db_plugin->mark_deposit_done (db_plugin->cls, - row_id); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + qs = db_plugin->aggregate (db_plugin->cls, + &au->h_payto, + &au->merchant_pub, + &au->wtid, + &au->total_amount); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to execute aggregation!\n"); + global_ret = EXIT_FAILURE; + return GNUNET_SYSERR; } - return qs; -} - - -/** - * Function called with details about another deposit we - * can aggregate into an existing aggregation unit. - * - * @param cls a `struct AggregationUnit` - * @param row_id identifies database entry - * @param coin_pub public key of the coin - * @param amount_with_fee amount that was deposited including fee - * @param deposit_fee amount the exchange gets to keep as transaction fees - * @param h_contract_terms hash of the proposal data known to merchant and customer - * @return transaction status code - */ -static enum GNUNET_DB_QueryStatus -aggregate_cb (void *cls, - uint64_t row_id, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_PrivateContractHash *h_contract_terms) -{ - struct AggregationUnit *au = cls; - struct TALER_Amount old; - enum GNUNET_DB_QueryStatus qs; - - if (au->rows_offset >= TALER_EXCHANGEDB_MATCHING_DEPOSITS_LIMIT) + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { - /* Bug: we asked for at most #TALER_EXCHANGEDB_MATCHING_DEPOSITS_LIMIT results! */ - GNUNET_break (0); - /* Skip this one, but keep going with the overall transaction */ - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + /* serializiability issue, try again */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Serialization issue, trying again later!\n"); + return GNUNET_NO; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Aggregation total is %s.\n", + TALER_amount2s (&au->total_amount)); + /* Subtract wire transfer fee and round to the unit supported by the + wire transfer method; Check if after rounding down, we still have + an amount to transfer, and if not mark as 'tiny'. */ + if (au->have_transient) + GNUNET_assert (0 <= + TALER_amount_add (&au->total_amount, + &au->total_amount, + &au->trans)); - /* add to total */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Adding transaction amount %s from row %llu to aggregation\n", - TALER_amount2s (amount_with_fee), - (unsigned long long) row_id); - /* save the existing total aggregate in 'old', for later */ - old = au->total_amount; - /* we begin with the total contribution of the current coin */ - au->total_amount = *amount_with_fee; - /* compute contribution of this coin (after fees) */ - au->have_refund = false; - qs = db_plugin->select_refunds_by_coin (db_plugin->cls, - coin_pub, - &au->merchant_pub, - h_contract_terms, - &refund_by_coin_cb, - au); - if (0 > qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; - } - if (! au->have_refund) - { - struct TALER_Amount tmp; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Subtracting deposit fee %s for non-refunded coin\n", - TALER_amount2s (deposit_fee)); - if (0 > - TALER_amount_subtract (&tmp, + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Rounding aggregate of %s\n", + TALER_amount2s (&au->total_amount)); + if ( (0 >= + TALER_amount_subtract (&au->final_amount, &au->total_amount, - deposit_fee)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Fatally malformed record at %llu over amount %s (deposit fee exceeds deposited value)\n", - (unsigned long long) row_id, - TALER_amount2s (&au->total_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (old.currency, - &au->total_amount)); - } + &au->fees.wire)) || + (GNUNET_SYSERR == + TALER_amount_round_down (&au->final_amount, + ¤cy_round_unit)) || + (TALER_amount_is_zero (&au->final_amount)) || + (! kyc_satisfied (au)) || + (! aml_satisfied (au)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Not ready for wire transfer (%d/%s)\n", + qs, + TALER_amount2s (&au->final_amount)); + if (au->have_transient) + qs = db_plugin->update_aggregation_transient (db_plugin->cls, + &au->h_payto, + &au->wtid, + au->requirement_row, + &au->total_amount); else + qs = db_plugin->create_aggregation_transient (db_plugin->cls, + &au->h_payto, + au->wa->section_name, + &au->merchant_pub, + &au->wtid, + au->requirement_row, + &au->total_amount); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { - au->total_amount = tmp; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Serialization issue, trying again later!\n"); + return GNUNET_NO; } - } - - /* now add the au->total_amount with the (remaining) contribution of - the current coin to the 'old' value with the current aggregate value */ - { - struct TALER_Amount tmp; - - if (0 > - TALER_amount_add (&tmp, - &au->total_amount, - &old)) + if (GNUNET_DB_STATUS_HARD_ERROR == qs) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Overflow or currency incompatibility during aggregation at %llu\n", - (unsigned long long) row_id); - /* Skip this one, but keep going! */ - au->total_amount = old; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + GNUNET_break (0); + global_ret = EXIT_FAILURE; + return GNUNET_SYSERR; } - au->total_amount = tmp; - } - - /* "append" to our list of rows */ - au->additional_rows[au->rows_offset++] = row_id; - /* insert into aggregation tracking table */ - qs = db_plugin->insert_aggregation_tracking (db_plugin->cls, - &au->wtid, - row_id); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; - } - qs = db_plugin->mark_deposit_done (db_plugin->cls, - row_id); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; + /* commit */ + return GNUNET_OK; } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Aggregator marked deposit %llu as DONE\n", - (unsigned long long) row_id); - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; -} - - -/** - * Perform a database commit. If it fails, print a warning. - * - * @return status of commit - */ -static enum GNUNET_DB_QueryStatus -commit_or_warn (void) -{ - enum GNUNET_DB_QueryStatus qs; - - qs = db_plugin->commit (db_plugin->cls); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return qs; - GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? GNUNET_ERROR_TYPE_INFO - : GNUNET_ERROR_TYPE_ERROR, - "Failed to commit database transaction!\n"); - return qs; -} - - -/** - * Release lock on shard @a s in the database. - * On error, terminates this process. - * - * @param[in] s shard to free (and memory to release) - */ -static void -release_shard (struct Shard *s) -{ - enum GNUNET_DB_QueryStatus qs; - qs = db_plugin->release_revolving_shard ( - db_plugin->cls, - "aggregator", - s->shard_start, - s->shard_end); - GNUNET_free (s); + qs = trigger_wire_transfer (au); switch (qs) { - case GNUNET_DB_STATUS_HARD_ERROR: case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Serialization issue during aggregation; trying again later!\n") + ; + return GNUNET_NO; + case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - GNUNET_SCHEDULER_shutdown (); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Strange, but let's just continue */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* normal case */ + global_ret = EXIT_FAILURE; + return GNUNET_SYSERR; + default: break; } + { + struct TALER_CoinDepositEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_DEPOSIT_STATUS_CHANGED), + .merchant_pub = au->merchant_pub + }; + + db_plugin->event_notify (db_plugin->cls, + &rep.header, + NULL, + 0); + } + return GNUNET_OK; + } -/** - * Main work function that queries the DB and aggregates transactions - * into larger wire transfers. - * - * @param cls a `struct Shard *` - */ static void run_aggregation (void *cls) { struct Shard *s = cls; struct AggregationUnit au_active; enum GNUNET_DB_QueryStatus qs; + enum GNUNET_GenericReturnValue ret; task = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Checking for ready deposits to aggregate\n"); + /* make sure we have current fees */ + memset (&au_active, + 0, + sizeof (au_active)); + au_active.execution_time = GNUNET_TIME_timestamp_get (); if (GNUNET_OK != db_plugin->start_deferred_wire_out (db_plugin->cls)) { @@ -705,16 +878,12 @@ run_aggregation (void *cls) release_shard (s); return; } - memset (&au_active, - 0, - sizeof (au_active)); qs = db_plugin->get_ready_deposit ( db_plugin->cls, s->shard_start, s->shard_end, - kyc_off ? true : false, - &deposit_cb, - &au_active); + &au_active.merchant_pub, + &au_active.payto_uri); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -753,202 +922,63 @@ run_aggregation (void *cls) (0 == counter) ) { /* in test mode, shutdown after a shard is done with 0 work */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No work done and in test mode, shutting down\n"); GNUNET_SCHEDULER_shutdown (); return; } GNUNET_assert (NULL == task); /* If we ended up doing zero work, sleep a bit */ if (0 == counter) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Going to sleep for %s before trying again\n", + GNUNET_TIME_relative2s (aggregator_idle_sleep_interval, + true)); task = GNUNET_SCHEDULER_add_delayed (aggregator_idle_sleep_interval, - &run_shard, + &drain_kyc_alerts, NULL); + } else - task = GNUNET_SCHEDULER_add_now (&run_shard, + { + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, NULL); + } return; } case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: s->work_counter++; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found ready deposit!\n"); /* continued below */ break; } - /* Now try to find other deposits to aggregate */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found ready deposit for %s, aggregating by target %llu\n", - TALER_B2S (&au_active.merchant_pub), - (unsigned long long) au_active.wire_target); - qs = db_plugin->iterate_matching_deposits (db_plugin->cls, - au_active.wire_target, - &au_active.merchant_pub, - &aggregate_cb, - &au_active, - TALER_EXCHANGEDB_MATCHING_DEPOSITS_LIMIT); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) + TALER_payto_hash (au_active.payto_uri, + &au_active.h_payto); + ret = do_aggregate (&au_active); + cleanup_au (&au_active); + switch (ret) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to execute deposit iteration!\n"); - cleanup_au (&au_active); - db_plugin->rollback (db_plugin->cls); + case GNUNET_SYSERR: global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); release_shard (s); return; - } - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - /* serializiability issue, try again */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Serialization issue, trying again later!\n"); + case GNUNET_NO: db_plugin->rollback (db_plugin->cls); - cleanup_au (&au_active); GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_aggregation, s); return; + case GNUNET_OK: + /* continued below */ + break; } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found %d other deposits to combine into wire transfer.\n", - qs); - - /* Subtract wire transfer fee and round to the unit supported by the - wire transfer method; Check if after rounding down, we still have - an amount to transfer, and if not mark as 'tiny'. */ - if ( (0 >= - TALER_amount_subtract (&au_active.final_amount, - &au_active.total_amount, - &au_active.wire_fee)) || - (GNUNET_SYSERR == - TALER_amount_round_down (&au_active.final_amount, - ¤cy_round_unit)) || - ( (0 == au_active.final_amount.value) && - (0 == au_active.final_amount.fraction) ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Aggregate value too low for transfer (%d/%s)\n", - qs, - TALER_amount2s (&au_active.final_amount)); - /* Rollback ongoing transaction, as we will not use the respective - WTID and thus need to remove the tracking data */ - db_plugin->rollback (db_plugin->cls); - /* There were results, just the value was too low. Start another - transaction to mark all* of the selected deposits as minor! */ - if (GNUNET_OK != - db_plugin->start (db_plugin->cls, - "aggregator mark tiny transactions")) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to start database transaction!\n"); - global_ret = EXIT_FAILURE; - cleanup_au (&au_active); - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; - } - /* Mark transactions by row_id as minor */ - qs = db_plugin->mark_deposit_tiny (db_plugin->cls, - au_active.row_id); - if (0 <= qs) - { - for (unsigned int i = 0; i<au_active.rows_offset; i++) - { - qs = db_plugin->mark_deposit_tiny (db_plugin->cls, - au_active.additional_rows[i]); - if (0 > qs) - break; - } - } - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Serialization issue, trying again later!\n"); - db_plugin->rollback (db_plugin->cls); - cleanup_au (&au_active); - /* start again */ - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; - } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - db_plugin->rollback (db_plugin->cls); - cleanup_au (&au_active); - global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; - } - /* commit */ - (void) commit_or_warn (); - cleanup_au (&au_active); - - /* start again */ - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; - } GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Preparing wire transfer of %s to %s\n", - TALER_amount2s (&au_active.final_amount), - TALER_B2S (&au_active.merchant_pub)); - { - void *buf; - size_t buf_size; - - TALER_BANK_prepare_transfer (au_active.payto_uri, - &au_active.final_amount, - exchange_base_url, - &au_active.wtid, - &buf, - &buf_size); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Storing %u bytes of wire prepare data\n", - (unsigned int) buf_size); - /* Commit our intention to execute the wire transfer! */ - qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, - au_active.wa->method, - buf, - buf_size); - GNUNET_free (buf); - } - /* Commit the WTID data to 'wire_out' to finally satisfy aggregation - table constraints */ - if (qs >= 0) - qs = db_plugin->store_wire_transfer_out (db_plugin->cls, - au_active.execution_time, - &au_active.wtid, - au_active.wire_target, - au_active.wa->section_name, - &au_active.final_amount); - cleanup_au (&au_active); - - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Serialization issue for prepared wire data; trying again later!\n"); - db_plugin->rollback (db_plugin->cls); - /* start again */ - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; - } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - db_plugin->rollback (db_plugin->cls); - /* die hard */ - global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Stored wire transfer out instructions\n"); + "Committing aggregation result\n"); /* Now we can finally commit the overall transaction, as we are again consistent if all of this passes. */ @@ -956,8 +986,8 @@ run_aggregation (void *cls) { case GNUNET_DB_STATUS_SOFT_ERROR: /* try again */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Commit issue for prepared wire data; trying again later!\n"); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Serialization issue on commit; trying again later!\n"); GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_aggregation, s); @@ -966,11 +996,12 @@ run_aggregation (void *cls) GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ release_shard (s); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Preparation complete, going again\n"); + "Commit complete, going again\n"); GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_aggregation, s); @@ -979,6 +1010,7 @@ run_aggregation (void *cls) GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ release_shard (s); return; } @@ -998,6 +1030,8 @@ run_shard (void *cls) (void) cls; task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running aggregation shard\n"); if (GNUNET_SYSERR == db_plugin->preflight (db_plugin->cls)) { @@ -1024,6 +1058,7 @@ run_shard (void *cls) GNUNET_free (s); delay = GNUNET_TIME_randomized_backoff (delay, GNUNET_TIME_UNIT_SECONDS); + GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_delayed (delay, &run_shard, NULL); @@ -1041,12 +1076,207 @@ run_shard (void *cls) "Starting shard [%u:%u]!\n", (unsigned int) s->shard_start, (unsigned int) s->shard_end); + GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_aggregation, s); } /** + * Function called on transient aggregations matching + * a particular hash of a payto URI. + * + * @param cls + * @param payto_uri corresponding payto URI + * @param wtid wire transfer identifier of transient aggregation + * @param merchant_pub public key of the merchant + * @param total amount aggregated so far + * @return true to continue to iterate + */ +static bool +handle_transient_cb ( + void *cls, + const char *payto_uri, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_Amount *total) +{ + struct AggregationUnit *au = cls; + + if (GNUNET_OK != au->ret) + { + GNUNET_break (0); + return false; + } + au->payto_uri = GNUNET_strdup (payto_uri); + au->wtid = *wtid; + au->merchant_pub = *merchant_pub; + au->trans = *total; + au->have_transient = true; + au->ret = do_aggregate (au); + GNUNET_free (au->payto_uri); + return (GNUNET_OK == au->ret); +} + + +static void +drain_kyc_alerts (void *cls) +{ + enum GNUNET_DB_QueryStatus qs; + struct AggregationUnit au; + + (void) cls; + task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Draining KYC alerts\n"); + memset (&au, + 0, + sizeof (au)); + au.execution_time = GNUNET_TIME_timestamp_get (); + if (GNUNET_SYSERR == + db_plugin->preflight (db_plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to obtain database connection!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + db_plugin->start (db_plugin->cls, + "handle kyc alerts")) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start database transaction!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + while (1) + { + qs = db_plugin->drain_kyc_alert (db_plugin->cls, + 1, + &au.h_payto); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + qs = db_plugin->commit (db_plugin->cls); + if (qs < 0) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to commit KYC drain\n"); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_shard, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* handled below */ + break; + } + + au.ret = GNUNET_OK; + qs = db_plugin->find_aggregation_transient (db_plugin->cls, + &au.h_payto, + &handle_transient_cb, + &au); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to lookup transient aggregates!\n"); + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* serializiability issue, try again */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Serialization issue, trying again later!\n"); + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + continue; /* while (1) */ + default: + break; + } + break; + } /* while(1) */ + + { + enum GNUNET_GenericReturnValue ret; + + ret = au.ret; + cleanup_au (&au); + switch (ret) + { + case GNUNET_SYSERR: + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ + return; + case GNUNET_NO: + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_OK: + /* continued below */ + break; + } + } + + switch (commit_or_warn ()) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + /* try again */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Serialization issue on commit; trying again later!\n"); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Commit complete, going again\n"); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + default: + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ + return; + } +} + + +/** * First task. * * @param cls closure, NULL @@ -1066,7 +1296,8 @@ run (void *cls, (void) cfgfile; cfg = c; - if (GNUNET_OK != parse_wirewatch_config ()) + if (GNUNET_OK != + parse_aggregator_config ()) { cfg = NULL; global_ret = EXIT_NOTCONFIGURED; @@ -1087,11 +1318,18 @@ run (void *cls, shard_size = 1U + INT32_MAX; else shard_size = (uint32_t) ass; + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_init (cfg)) + { + cfg = NULL; + global_ret = EXIT_NOTCONFIGURED; + return; + } + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + NULL); GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_shard, + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, NULL); - GNUNET_SCHEDULER_add_shutdown (&shutdown_task, - cls); } diff --git a/src/exchange/taler-exchange-closer.c b/src/exchange/taler-exchange-closer.c index 3847e05b2..779525c4e 100644 --- a/src/exchange/taler-exchange-closer.c +++ b/src/exchange/taler-exchange-closer.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2016-2021 Taler Systems SA + Copyright (C) 2016-2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -204,19 +204,25 @@ commit_or_warn (void) * @param account_payto_uri information about the bank account that initially * caused the reserve to be created * @param expiration_date when did the reserve expire - * @return transaction status code + * @param close_request_row row of request asking for + * closure, 0 for expired reserves + * @return #GNUNET_OK on success (continue) + * #GNUNET_NO on non-fatal errors (try again) + * #GNUNET_SYSERR on fatal errors (abort) */ -static enum GNUNET_DB_QueryStatus +static enum GNUNET_GenericReturnValue expired_reserve_cb (void *cls, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_Amount *left, const char *account_payto_uri, - struct GNUNET_TIME_Timestamp expiration_date) + struct GNUNET_TIME_Timestamp expiration_date, + uint64_t close_request_row) { struct GNUNET_TIME_Timestamp now; struct TALER_WireTransferIdentifierRawP wtid; struct TALER_Amount amount_without_fee; struct TALER_Amount closing_fee; + struct TALER_WireFeeSet fees; enum TALER_AmountArithmeticResult ret; enum GNUNET_DB_QueryStatus qs; const struct TALER_EXCHANGEDB_AccountInfo *wa; @@ -238,13 +244,12 @@ expired_reserve_cb (void *cls, account_payto_uri); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; } - /* lookup `closing_fee` from time of actual reserve expiration + /* lookup `fees` from time of actual reserve expiration (we may be lagging behind!) */ { - struct TALER_Amount wire_fee; struct GNUNET_TIME_Timestamp start_date; struct GNUNET_TIME_Timestamp end_date; struct TALER_MasterSignatureP master_sig; @@ -255,20 +260,29 @@ expired_reserve_cb (void *cls, expiration_date, &start_date, &end_date, - &wire_fee, - &closing_fee, + &fees, &master_sig); - if (0 >= qs) + switch (qs) { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Could not get wire fees for %s at %s. Aborting run.\n", wa->method, GNUNET_TIME_timestamp2s (expiration_date)); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + return GNUNET_NO; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* continued below */ + break; } } /* calculate transfer amount */ + closing_fee = fees.closing; ret = TALER_amount_subtract (&amount_without_fee, left, &closing_fee); @@ -281,8 +295,8 @@ expired_reserve_cb (void *cls, GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (left->currency, &amount_without_fee)); + ret = TALER_AAR_RESULT_ZERO; } - GNUNET_assert (TALER_AAR_RESULT_POSITIVE == ret); /* round down to enable transfer */ if (GNUNET_SYSERR == TALER_amount_round_down (&amount_without_fee, @@ -291,27 +305,25 @@ expired_reserve_cb (void *cls, GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; } /* NOTE: sizeof (*reserve_pub) == sizeof (wtid) right now, but to be future-compatible, we use the memset + min construction */ memset (&wtid, 0, sizeof (wtid)); - memcpy (&wtid, - reserve_pub, - GNUNET_MIN (sizeof (wtid), - sizeof (*reserve_pub))); - if (TALER_AAR_INVALID_NEGATIVE_RESULT != ret) - qs = db_plugin->insert_reserve_closed (db_plugin->cls, - reserve_pub, - now, - account_payto_uri, - &wtid, - left, - &closing_fee); - else - qs = GNUNET_DB_STATUS_HARD_ERROR; + GNUNET_memcpy (&wtid, + reserve_pub, + GNUNET_MIN (sizeof (wtid), + sizeof (*reserve_pub))); + qs = db_plugin->insert_reserve_closed (db_plugin->cls, + reserve_pub, + now, + account_payto_uri, + &wtid, + left, + &closing_fee, + close_request_row); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Closing reserve %s over %s (%d, %d)\n", TALER_B2S (reserve_pub), @@ -319,22 +331,30 @@ expired_reserve_cb (void *cls, (int) ret, qs); /* Check for hard failure */ - if ( (TALER_AAR_INVALID_NEGATIVE_RESULT == ret) || - (GNUNET_DB_STATUS_HARD_ERROR == qs) ) + if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; } - if ( (TALER_AAR_RESULT_ZERO == ret) || - (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) ) + if (TALER_amount_is_zero (&amount_without_fee)) { /* Reserve balance was zero OR soft error */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Reserve was virtually empty, moving on\n"); - (void) commit_or_warn (); - return qs; + qs = commit_or_warn (); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + return GNUNET_NO; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return GNUNET_OK; + } } /* success, perform wire transfer */ @@ -355,19 +375,25 @@ expired_reserve_cb (void *cls, buf_size); GNUNET_free (buf); } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) + switch (qs) { + case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: /* start again */ - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + return GNUNET_NO; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; } - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + return GNUNET_OK; } @@ -409,11 +435,18 @@ run_reserve_closures (void *cls) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Checking for reserves to close by date %s\n", GNUNET_TIME_timestamp2s (now)); - qs = db_plugin->get_expired_reserves (db_plugin->cls, - now, - &expired_reserve_cb, - NULL); - GNUNET_assert (1 >= qs); + qs = db_plugin->get_unfinished_close_requests (db_plugin->cls, + &expired_reserve_cb, + NULL); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* Try expired reserves as well */ + qs = db_plugin->get_expired_reserves ( + db_plugin->cls, + now, + &expired_reserve_cb, + NULL); + } switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -436,13 +469,11 @@ run_reserve_closures (void *cls) if (GNUNET_YES == test_mode) { GNUNET_SCHEDULER_shutdown (); + return; } - else - { - task = GNUNET_SCHEDULER_add_delayed (closer_idle_sleep_interval, - &run_reserve_closures, - NULL); - } + task = GNUNET_SCHEDULER_add_delayed (closer_idle_sleep_interval, + &run_reserve_closures, + NULL); return; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: (void) commit_or_warn (); diff --git a/src/exchange/taler-exchange-drain.c b/src/exchange/taler-exchange-drain.c new file mode 100644 index 000000000..d409487c1 --- /dev/null +++ b/src/exchange/taler-exchange-drain.c @@ -0,0 +1,431 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-drain.c + * @brief Process that drains exchange profits from the escrow account + * and puts them into some regular account of the exchange. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <pthread.h> +#include "taler_exchangedb_lib.h" +#include "taler_exchangedb_plugin.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" + + +/** + * The exchange's configuration. + */ +static const struct GNUNET_CONFIGURATION_Handle *cfg; + +/** + * Our database plugin. + */ +static struct TALER_EXCHANGEDB_Plugin *db_plugin; + +/** + * Our master public key. + */ +static struct TALER_MasterPublicKeyP master_pub; + +/** + * Next task to run, if any. + */ +static struct GNUNET_SCHEDULER_Task *task; + +/** + * Base URL of this exchange. + */ +static char *exchange_base_url; + +/** + * Value to return from main(). 0 on success, non-zero on errors. + */ +static int global_ret; + + +/** + * We're being aborted with CTRL-C (or SIGTERM). Shut down. + * + * @param cls closure + */ +static void +shutdown_task (void *cls) +{ + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running shutdown\n"); + if (NULL != task) + { + GNUNET_SCHEDULER_cancel (task); + task = NULL; + } + db_plugin->rollback (db_plugin->cls); /* just in case */ + TALER_EXCHANGEDB_plugin_unload (db_plugin); + db_plugin = NULL; + TALER_EXCHANGEDB_unload_accounts (); + cfg = NULL; +} + + +/** + * Parse the configuration for drain. + * + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_drain_config (void) +{ + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "BASE_URL", + &exchange_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL"); + return GNUNET_SYSERR; + } + + { + char *master_public_key_str; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "MASTER_PUBLIC_KEY", + &master_public_key_str)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "MASTER_PUBLIC_KEY"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_public_key_from_string (master_public_key_str, + strlen ( + master_public_key_str), + &master_pub.eddsa_pub)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "MASTER_PUBLIC_KEY", + "invalid base32 encoding for a master public key"); + GNUNET_free (master_public_key_str); + return GNUNET_SYSERR; + } + GNUNET_free (master_public_key_str); + } + if (NULL == + (db_plugin = TALER_EXCHANGEDB_plugin_load (cfg))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to initialize DB subsystem\n"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_EXCHANGEDB_load_accounts (cfg, + TALER_EXCHANGEDB_ALO_DEBIT + | TALER_EXCHANGEDB_ALO_AUTHDATA)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No wire accounts configured for debit!\n"); + TALER_EXCHANGEDB_plugin_unload (db_plugin); + db_plugin = NULL; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Perform a database commit. If it fails, print a warning. + * + * @return status of commit + */ +static enum GNUNET_DB_QueryStatus +commit_or_warn (void) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->commit (db_plugin->cls); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return qs; + GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? GNUNET_ERROR_TYPE_INFO + : GNUNET_ERROR_TYPE_ERROR, + "Failed to commit database transaction!\n"); + return qs; +} + + +/** + * Execute a wire drain. + * + * @param cls NULL + */ +static void +run_drain (void *cls) +{ + enum GNUNET_DB_QueryStatus qs; + uint64_t serial; + struct TALER_WireTransferIdentifierRawP wtid; + char *account_section; + char *payto_uri; + struct GNUNET_TIME_Timestamp request_timestamp; + struct TALER_Amount amount; + struct TALER_MasterSignatureP master_sig; + + (void) cls; + task = NULL; + if (GNUNET_OK != + db_plugin->start (db_plugin->cls, + "run drain")) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start database transaction!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + qs = db_plugin->profit_drains_get_pending (db_plugin->cls, + &serial, + &wtid, + &account_section, + &payto_uri, + &request_timestamp, + &amount, + &master_sig); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + db_plugin->rollback (db_plugin->cls); + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + db_plugin->rollback (db_plugin->cls); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Serialization failure on simple SELECT!?\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* no profit drains, finished */ + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "No profit drains pending. Exiting.\n"); + GNUNET_SCHEDULER_shutdown (); + return; + default: + /* continued below */ + break; + } + /* Check signature (again, this is a critical operation!) */ + if (GNUNET_OK != + TALER_exchange_offline_profit_drain_verify ( + &wtid, + request_timestamp, + &amount, + account_section, + payto_uri, + &master_pub, + &master_sig)) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + GNUNET_SCHEDULER_shutdown (); + return; + } + + /* Display data for manual human check */ + fprintf (stdout, + "Critical operation. MANUAL CHECK REQUIRED.\n"); + fprintf (stdout, + "We will wire %s to `%s'\n based on instructions from %s.\n", + TALER_amount2s (&amount), + payto_uri, + GNUNET_TIME_timestamp2s (request_timestamp)); + fprintf (stdout, + "Press ENTER to confirm, CTRL-D to abort.\n"); + while (1) + { + int key; + + key = getchar (); + if (EOF == key) + { + fprintf (stdout, + "Transfer aborted.\n" + "Re-run 'taler-exchange-drain' to try it again.\n" + "Contact Taler Systems SA to cancel it for good.\n" + "Exiting.\n"); + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_FAILURE; + return; + } + if ('\n' == key) + break; + } + + /* Note: account_section ignored for now, we + might want to use it here in the future... */ + (void) account_section; + { + char *method; + void *buf; + size_t buf_size; + + TALER_BANK_prepare_transfer (payto_uri, + &amount, + exchange_base_url, + &wtid, + &buf, + &buf_size); + method = TALER_payto_get_method (payto_uri); + qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, + method, + buf, + buf_size); + GNUNET_free (method); + GNUNET_free (buf); + } + qs = db_plugin->profit_drains_set_finished (db_plugin->cls, + serial); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + db_plugin->rollback (db_plugin->cls); + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + db_plugin->rollback (db_plugin->cls); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed: database serialization issue\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + default: + /* continued below */ + break; + } + /* commit transaction + report success + exit */ + if (0 >= commit_or_warn ()) + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "Profit drain triggered. Exiting.\n"); + GNUNET_SCHEDULER_shutdown (); +} + + +/** + * First task. + * + * @param cls closure, NULL + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + (void) cls; + (void) args; + (void) cfgfile; + + cfg = c; + if (GNUNET_OK != parse_drain_config ()) + { + cfg = NULL; + global_ret = EXIT_NOTCONFIGURED; + return; + } + if (GNUNET_SYSERR == + db_plugin->preflight (db_plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to obtain database connection!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_drain, + NULL); + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + cls); +} + + +/** + * The main function of the taler-exchange-drain. + * + * @param argc number of arguments from the command line + * @param argv command line arguments + * @return 0 ok, 1 on error + */ +int +main (int argc, + char *const *argv) +{ + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION), + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + if (GNUNET_OK != + GNUNET_STRINGS_get_utf8_args (argc, argv, + &argc, &argv)) + return EXIT_INVALIDARGUMENT; + TALER_OS_init (); + ret = GNUNET_PROGRAM_run ( + argc, argv, + "taler-exchange-drain", + gettext_noop ( + "process that executes a single profit drain"), + options, + &run, NULL); + GNUNET_free_nz ((void *) argv); + if (GNUNET_SYSERR == ret) + return EXIT_INVALIDARGUMENT; + if (GNUNET_NO == ret) + return EXIT_SUCCESS; + return global_ret; +} + + +/* end of taler-exchange-drain.c */ diff --git a/src/exchange/taler-exchange-expire.c b/src/exchange/taler-exchange-expire.c new file mode 100644 index 000000000..b2d34ee1c --- /dev/null +++ b/src/exchange/taler-exchange-expire.c @@ -0,0 +1,500 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +/** + * @file taler-exchange-expire.c + * @brief Process that cleans up expired purses + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <pthread.h> +#include "taler_exchangedb_lib.h" +#include "taler_exchangedb_plugin.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" + + +/** + * Work shard we are processing. + */ +struct Shard +{ + + /** + * When did we start processing the shard? + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * Starting row of the shard. + */ + struct GNUNET_TIME_Absolute shard_start; + + /** + * Inclusive end row of the shard. + */ + struct GNUNET_TIME_Absolute shard_end; + + /** + * Number of starting points found in the shard. + */ + uint64_t work_counter; + +}; + + +/** + * The exchange's configuration. + */ +static const struct GNUNET_CONFIGURATION_Handle *cfg; + +/** + * Our database plugin. + */ +static struct TALER_EXCHANGEDB_Plugin *db_plugin; + +/** + * Next task to run, if any. + */ +static struct GNUNET_SCHEDULER_Task *task; + +/** + * How big are the shards we are processing? Is an inclusive offset, so every + * shard ranges from [X,X+shard_size) exclusive. So a shard covers + * shard_size slots. + */ +static struct GNUNET_TIME_Relative shard_size; + +/** + * Value to return from main(). 0 on success, non-zero on errors. + */ +static int global_ret; + +/** + * #GNUNET_YES if we are in test mode and should exit when idle. + */ +static int test_mode; + +/** + * If this is a first-time run, we immediately + * try to catch up with the present. + */ +static bool jump_mode; + + +/** + * Select a shard to work on. + * + * @param cls NULL + */ +static void +run_shard (void *cls); + + +/** + * We're being aborted with CTRL-C (or SIGTERM). Shut down. + * + * @param cls closure + */ +static void +shutdown_task (void *cls) +{ + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running shutdown\n"); + if (NULL != task) + { + GNUNET_SCHEDULER_cancel (task); + task = NULL; + } + TALER_EXCHANGEDB_plugin_unload (db_plugin); + db_plugin = NULL; + cfg = NULL; +} + + +/** + * Parse the configuration for expire. + * + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_expire_config (void) +{ + if (NULL == + (db_plugin = TALER_EXCHANGEDB_plugin_load (cfg))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to initialize DB subsystem\n"); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Perform a database commit. If it fails, print a warning. + * + * @return status of commit + */ +static enum GNUNET_DB_QueryStatus +commit_or_warn (void) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->commit (db_plugin->cls); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return qs; + GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? GNUNET_ERROR_TYPE_INFO + : GNUNET_ERROR_TYPE_ERROR, + "Failed to commit database transaction!\n"); + return qs; +} + + +/** + * Release lock on shard @a s in the database. + * On error, terminates this process. + * + * @param[in] s shard to free (and memory to release) + */ +static void +release_shard (struct Shard *s) +{ + enum GNUNET_DB_QueryStatus qs; + unsigned long long wc = (unsigned long long) s->work_counter; + + qs = db_plugin->complete_shard ( + db_plugin->cls, + "expire", + s->shard_start.abs_value_us, + s->shard_end.abs_value_us); + GNUNET_free (s); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Strange, but let's just continue */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Purse expiration shard completed with %llu purses\n", + wc); + /* normal case */ + break; + } + if ( (0 == wc) && + (test_mode) && + (! jump_mode) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "In test-mode without work. Terminating.\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } +} + + +/** + * Release lock on shard @a s in the database due to an abort of the + * operation. On error, terminates this process. + * + * @param[in] s shard to free (and memory to release) + */ +static void +abort_shard (struct Shard *s) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->abort_shard (db_plugin->cls, + "expire", + s->shard_start.abs_value_us, + s->shard_end.abs_value_us); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to abort shard (%d)!\n", + qs); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } +} + + +/** + * Main function that processes the work in one shard. + * + * @param[in] cls a `struct Shard` to process + */ +static void +run_expire (void *cls) +{ + struct Shard *s = cls; + enum GNUNET_DB_QueryStatus qs; + + task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking for expired purses\n"); + if (GNUNET_SYSERR == + db_plugin->preflight (db_plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to obtain database connection!\n"); + abort_shard (s); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + db_plugin->start (db_plugin->cls, + "expire-purse")) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls); + abort_shard (s); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + qs = db_plugin->expire_purse (db_plugin->cls, + s->shard_start, + s->shard_end); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls); + abort_shard (s); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + db_plugin->rollback (db_plugin->cls); + abort_shard (s); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_shard, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (0 > commit_or_warn ()) + { + db_plugin->rollback (db_plugin->cls); + abort_shard (s); + } + else + { + release_shard (s); + } + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_shard, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* commit, and go again immediately */ + s->work_counter++; + (void) commit_or_warn (); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_expire, + s); + } +} + + +/** + * Select a shard to work on. + * + * @param cls NULL + */ +static void +run_shard (void *cls) +{ + struct Shard *s; + enum GNUNET_DB_QueryStatus qs; + + (void) cls; + task = NULL; + if (GNUNET_SYSERR == + db_plugin->preflight (db_plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to obtain database connection!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + s = GNUNET_new (struct Shard); + s->start_time = GNUNET_TIME_timestamp_get (); + qs = db_plugin->begin_shard (db_plugin->cls, + "expire", + shard_size, + jump_mode + ? GNUNET_TIME_absolute_subtract ( + GNUNET_TIME_absolute_get (), + shard_size). + abs_value_us + : shard_size.rel_value_us, + &s->shard_start.abs_value_us, + &s->shard_end.abs_value_us); + jump_mode = false; + if (0 >= qs) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + static struct GNUNET_TIME_Relative delay; + + GNUNET_free (s); + delay = GNUNET_TIME_randomized_backoff (delay, + GNUNET_TIME_UNIT_SECONDS); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_delayed (delay, + &run_shard, + NULL); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to begin shard (%d)!\n", + qs); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_TIME_absolute_is_future (s->shard_end)) + { + abort_shard (s); + if (test_mode) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "In test-mode without work. Terminating.\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_at (s->shard_end, + &run_shard, + NULL); + return; + } + /* If this is a first-time run, we immediately + try to catch up with the present */ + if (GNUNET_TIME_absolute_is_zero (s->shard_start)) + jump_mode = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting shard [%llu:%llu)!\n", + (unsigned long long) s->shard_start.abs_value_us, + (unsigned long long) s->shard_end.abs_value_us); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_expire, + s); +} + + +/** + * First task. + * + * @param cls closure, NULL + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + (void) cls; + (void) args; + (void) cfgfile; + + cfg = c; + if (GNUNET_OK != parse_expire_config ()) + { + cfg = NULL; + global_ret = EXIT_NOTCONFIGURED; + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + "exchange", + "EXPIRE_SHARD_SIZE", + &shard_size)) + { + cfg = NULL; + global_ret = EXIT_NOTCONFIGURED; + return; + } + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_shard, + NULL); + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + cls); +} + + +/** + * The main function of the taler-exchange-expire. + * + * @param argc number of arguments from the command line + * @param argv command line arguments + * @return 0 ok, non-zero on error, see #global_ret + */ +int +main (int argc, + char *const *argv) +{ + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_timetravel ('T', + "timetravel"), + GNUNET_GETOPT_option_flag ('t', + "test", + "run in test mode and exit when idle", + &test_mode), + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + if (GNUNET_OK != + GNUNET_STRINGS_get_utf8_args (argc, argv, + &argc, &argv)) + return EXIT_INVALIDARGUMENT; + TALER_OS_init (); + ret = GNUNET_PROGRAM_run ( + argc, argv, + "taler-exchange-expire", + gettext_noop ( + "background process that expires purses"), + options, + &run, NULL); + GNUNET_free_nz ((void *) argv); + if (GNUNET_SYSERR == ret) + return EXIT_INVALIDARGUMENT; + if (GNUNET_NO == ret) + return EXIT_SUCCESS; + return global_ret; +} + + +/* end of taler-exchange-expire.c */ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index 59398c6fc..36459fbd7 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.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 Affero General Public License as published by the Free Software @@ -25,32 +25,52 @@ #include <jansson.h> #include <microhttpd.h> #include <sched.h> -#include <pthread.h> #include <sys/resource.h> #include <limits.h> +#include "taler_kyclogic_lib.h" +#include "taler_templating_lib.h" #include "taler_mhd_lib.h" +#include "taler-exchange-httpd_age-withdraw.h" +#include "taler-exchange-httpd_age-withdraw_reveal.h" +#include "taler-exchange-httpd_aml-decision.h" #include "taler-exchange-httpd_auditors.h" -#include "taler-exchange-httpd_deposit.h" +#include "taler-exchange-httpd_batch-deposit.h" +#include "taler-exchange-httpd_batch-withdraw.h" +#include "taler-exchange-httpd_coins_get.h" +#include "taler-exchange-httpd_config.h" +#include "taler-exchange-httpd_contract.h" +#include "taler-exchange-httpd_csr.h" #include "taler-exchange-httpd_deposits_get.h" #include "taler-exchange-httpd_extensions.h" #include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_kyc-check.h" #include "taler-exchange-httpd_kyc-proof.h" #include "taler-exchange-httpd_kyc-wallet.h" +#include "taler-exchange-httpd_kyc-webhook.h" #include "taler-exchange-httpd_link.h" #include "taler-exchange-httpd_management.h" #include "taler-exchange-httpd_melt.h" #include "taler-exchange-httpd_metrics.h" #include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_purses_create.h" +#include "taler-exchange-httpd_purses_deposit.h" +#include "taler-exchange-httpd_purses_get.h" +#include "taler-exchange-httpd_purses_delete.h" +#include "taler-exchange-httpd_purses_merge.h" #include "taler-exchange-httpd_recoup.h" #include "taler-exchange-httpd_recoup-refresh.h" #include "taler-exchange-httpd_refreshes_reveal.h" #include "taler-exchange-httpd_refund.h" +#include "taler-exchange-httpd_reserves_attest.h" +#include "taler-exchange-httpd_reserves_close.h" #include "taler-exchange-httpd_reserves_get.h" +#include "taler-exchange-httpd_reserves_get_attest.h" +#include "taler-exchange-httpd_reserves_history.h" +#include "taler-exchange-httpd_reserves_open.h" +#include "taler-exchange-httpd_reserves_purse.h" +#include "taler-exchange-httpd_spa.h" #include "taler-exchange-httpd_terms.h" #include "taler-exchange-httpd_transfers_get.h" -#include "taler-exchange-httpd_wire.h" -#include "taler-exchange-httpd_withdraw.h" #include "taler_exchangedb_lib.h" #include "taler_exchangedb_plugin.h" #include "taler_extensions.h" @@ -62,6 +82,11 @@ #define UNIX_BACKLOG 50 /** + * How often will we try to connect to the database before giving up? + */ +#define MAX_DB_RETRIES 5 + +/** * Above what request latency do we start to log? */ #define WARN_LATENCY GNUNET_TIME_relative_multiply ( \ @@ -86,14 +111,17 @@ static int allow_address_reuse; const struct GNUNET_CONFIGURATION_Handle *TEH_cfg; /** - * Handle to the HTTP server. + * Configuration of age restriction + * + * Set after loading the library, enabled in database event handler. */ -static struct MHD_Daemon *mhd; +bool TEH_age_restriction_enabled = false; +struct TALER_AgeRestrictionConfig TEH_age_restriction_config = {0}; /** - * Our KYC configuration. + * Handle to the HTTP server. */ -struct TEH_KycOptions TEH_kyc_config; +static struct MHD_Daemon *mhd; /** * How long is caching /keys allowed at most? (global) @@ -112,16 +140,58 @@ struct GNUNET_TIME_Relative TEH_reserve_closing_delay; struct TALER_MasterPublicKeyP TEH_master_public_key; /** + * Key used to encrypt KYC attribute data in our database. + */ +struct TALER_AttributeEncryptionKeyP TEH_attribute_key; + +/** * Our DB plugin. (global) */ struct TALER_EXCHANGEDB_Plugin *TEH_plugin; /** + * Absolute STEFAN parameter. + */ +struct TALER_Amount TEH_stefan_abs; + +/** + * Logarithmic STEFAN parameter. + */ +struct TALER_Amount TEH_stefan_log; + +/** + * Linear STEFAN parameter. + */ +float TEH_stefan_lin; + +/** + * Where to redirect users from "/"? + */ +static char *toplevel_redirect_url; + +/** * Our currency. */ char *TEH_currency; /** + * Name of the KYC-AML-trigger evaluation binary. + */ +char *TEH_kyc_aml_trigger; + +/** + * Option set to #GNUNET_YES if rewards are enabled. + */ +int TEH_enable_rewards; + +/** + * What is the largest amount we allow a peer to + * merge into a reserve before always triggering + * an AML check? + */ +struct TALER_Amount TEH_aml_threshold; + +/** * Our base URL. */ char *TEH_base_url; @@ -148,9 +218,12 @@ int TEH_check_invariants_flag; bool TEH_suicide; /** - * Global register of extensions + * Signature of the configuration of all enabled extensions, + * signed by the exchange's offline master key with purpose + * TALER_SIGNATURE_MASTER_EXTENSION. */ -struct TALER_Extension **TEH_extensions; +struct TALER_MasterSignatureP TEH_extensions_sig; +bool TEH_extensions_signed = false; /** * Value to return from main() @@ -180,12 +253,28 @@ static unsigned long long active_connections; static unsigned long long req_max; /** + * Length of the cspecs array. + */ +static unsigned int num_cspecs; + +/** + * Rendering specs for currencies. + */ +static struct TALER_CurrencySpecification *cspecs; + +/** + * Rendering spec for our currency. + */ +const struct TALER_CurrencySpecification *TEH_cspec; + + +/** * Context for all CURL operations (useful to the event loop) */ struct GNUNET_CURL_Context *TEH_curl_ctx; /** - * Context for integrating #exchange_curl_ctx with the + * Context for integrating #TEH_curl_ctx with the * GNUnet event loop. */ static struct GNUNET_CURL_RescheduleContext *exchange_curl_rc; @@ -228,8 +317,7 @@ r404 (struct MHD_Connection *connection, * * @param rc request context * @param root uploaded JSON data - * @param args array of additional options (first must be the - * reserve public key, the second one should be "withdraw") + * @param args array of additional options * @return MHD result code */ static MHD_RESULT @@ -252,10 +340,6 @@ handle_post_coins (struct TEH_RequestContext *rc, } h[] = { { - .op = "deposit", - .handler = &TEH_handler_deposit - }, - { .op = "melt", .handler = &TEH_handler_melt }, @@ -301,6 +385,572 @@ handle_post_coins (struct TEH_RequestContext *rc, /** + * Handle a GET "/coins/$COIN_PUB[/$OP]" request. Parses the "coin_pub" + * EdDSA key of the coin and demultiplexes based on $OP. + * + * @param rc request context + * @param args array of additional options + * @return MHD result code + */ +static MHD_RESULT +handle_get_coins (struct TEH_RequestContext *rc, + const char *const args[2]) +{ + struct TALER_CoinSpendPublicKeyP coin_pub; + + if (NULL == args[0]) + { + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &coin_pub, + sizeof (coin_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB, + args[0]); + } + if (NULL != args[1]) + { + if (0 == strcmp (args[1], + "history")) + return TEH_handler_coins_get (rc, + &coin_pub); + if (0 == strcmp (args[1], + "link")) + return TEH_handler_link (rc, + &coin_pub); + } + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); +} + + +/** + * Signature of functions that handle operations + * authorized by AML officers. + * + * @param rc request context + * @param officer_pub the public key of the AML officer + * @param root uploaded JSON data + * @return MHD result code + */ +typedef MHD_RESULT +(*AmlOpPostHandler)(struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const json_t *root); + + +/** + * Handle a "/aml/$OFFICER_PUB/$OP" POST request. Parses the "officer_pub" + * EdDSA key of the officer and demultiplexes based on $OP. + * + * @param rc request context + * @param root uploaded JSON data + * @param args array of additional options + * @return MHD result code + */ +static MHD_RESULT +handle_post_aml (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]) +{ + struct TALER_AmlOfficerPublicKeyP officer_pub; + static const struct + { + /** + * Name of the operation (args[1]) + */ + const char *op; + + /** + * Function to call to perform the operation. + */ + AmlOpPostHandler handler; + + } h[] = { + { + .op = "decision", + .handler = &TEH_handler_post_aml_decision + }, + { + .op = NULL, + .handler = NULL + }, + }; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &officer_pub, + sizeof (officer_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED, + args[0]); + } + for (unsigned int i = 0; NULL != h[i].op; i++) + if (0 == strcmp (h[i].op, + args[1])) + return h[i].handler (rc, + &officer_pub, + root); + return r404 (rc->connection, + args[1]); +} + + +/** + * Signature of functions that handle operations + * authorized by AML officers. + * + * @param rc request context + * @param officer_pub the public key of the AML officer + * @param args remaining arguments + * @return MHD result code + */ +typedef MHD_RESULT +(*AmlOpGetHandler)(struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +/** + * Handle a "/aml/$OFFICER_PUB/$OP" GET request. Parses the "officer_pub" + * EdDSA key of the officer, checks the authentication signature, and + * demultiplexes based on $OP. + * + * @param rc request context + * @param args array of additional options + * @return MHD result code + */ +static MHD_RESULT +handle_get_aml (struct TEH_RequestContext *rc, + const char *const args[]) +{ + struct TALER_AmlOfficerPublicKeyP officer_pub; + static const struct + { + /** + * Name of the operation (args[1]) + */ + const char *op; + + /** + * Function to call to perform the operation. + */ + AmlOpGetHandler handler; + + } h[] = { + { + .op = "decisions", + .handler = &TEH_handler_aml_decisions_get + }, + { + .op = "decision", + .handler = &TEH_handler_aml_decision_get + }, + { + .op = NULL, + .handler = NULL + }, + }; + + if (NULL == args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED, + "argument missing"); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &officer_pub, + sizeof (officer_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED, + args[0]); + } + if (NULL == args[1]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS, + "AML GET operations must specify an operation identifier"); + } + { + const char *sig_hdr; + struct TALER_AmlOfficerSignatureP officer_sig; + + sig_hdr = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + TALER_AML_OFFICER_SIGNATURE_HEADER); + if ( (NULL == sig_hdr) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (sig_hdr, + strlen (sig_hdr), + &officer_sig, + sizeof (officer_sig))) || + (GNUNET_OK != + TALER_officer_aml_query_verify (&officer_pub, + &officer_sig)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID, + sig_hdr); + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->test_aml_officer (TEH_plugin->cls, + &officer_pub); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + for (unsigned int i = 0; NULL != h[i].op; i++) + if (0 == strcmp (h[i].op, + args[1])) + return h[i].handler (rc, + &officer_pub, + &args[2]); + return r404 (rc->connection, + args[1]); +} + + +/** + * Handle a "/age-withdraw/$ACH/reveal" POST request. Parses the "ACH" + * hash of the commitment from a previous call to + * /reserves/$reserve_pub/age-withdraw + * + * @param rc request context + * @param root uploaded JSON data + * @param args array of additional options + * @return MHD result code + */ +static MHD_RESULT +handle_post_age_withdraw (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]) +{ + struct TALER_AgeWithdrawCommitmentHashP ach; + + if (0 != strcmp ("reveal", args[1])) + return r404 (rc->connection, + args[1]); + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &ach, + sizeof (ach))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, + args[0]); + } + + return TEH_handler_age_withdraw_reveal (rc, + &ach, + root); +} + + +/** + * Signature of functions that handle operations on reserves. + * + * @param rc request context + * @param reserve_pub the public key of the reserve + * @param root uploaded JSON data + * @return MHD result code + */ +typedef MHD_RESULT +(*ReserveOpHandler)(struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + + +/** + * Handle a "/reserves/$RESERVE_PUB/$OP" POST request. Parses the "reserve_pub" + * EdDSA key of the reserve and demultiplexes based on $OP. + * + * @param rc request context + * @param root uploaded JSON data + * @param args array of additional options + * @return MHD result code + */ +static MHD_RESULT +handle_post_reserves (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]) +{ + struct TALER_ReservePublicKeyP reserve_pub; + static const struct + { + /** + * Name of the operation (args[1]) + */ + const char *op; + + /** + * Function to call to perform the operation. + */ + ReserveOpHandler handler; + + } h[] = { + { + .op = "batch-withdraw", + .handler = &TEH_handler_batch_withdraw + }, + { + .op = "age-withdraw", + .handler = &TEH_handler_age_withdraw + }, + { + .op = "purse", + .handler = &TEH_handler_reserves_purse + }, + { + .op = "open", + .handler = &TEH_handler_reserves_open + }, + { + .op = "close", + .handler = &TEH_handler_reserves_close + }, + { + .op = NULL, + .handler = NULL + }, + }; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &reserve_pub, + sizeof (reserve_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, + args[0]); + } + for (unsigned int i = 0; NULL != h[i].op; i++) + if (0 == strcmp (h[i].op, + args[1])) + return h[i].handler (rc, + &reserve_pub, + root); + return r404 (rc->connection, + args[1]); +} + + +/** + * Signature of functions that handle GET operations on reserves. + * + * @param rc request context + * @param reserve_pub the public key of the reserve + * @return MHD result code + */ +typedef MHD_RESULT +(*ReserveGetOpHandler)(struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub); + + +/** + * Handle a "GET /reserves/$RESERVE_PUB[/$OP]" request. Parses the "reserve_pub" + * EdDSA key of the reserve and demultiplexes based on $OP. + * + * @param rc request context + * @param args NULL-terminated array of additional options, zero, one or two + * @return MHD result code + */ +static MHD_RESULT +handle_get_reserves (struct TEH_RequestContext *rc, + const char *const args[]) +{ + struct TALER_ReservePublicKeyP reserve_pub; + static const struct + { + /** + * Name of the operation (args[1]), optional + */ + const char *op; + + /** + * Function to call to perform the operation. + */ + ReserveGetOpHandler handler; + + } h[] = { + { + .op = NULL, + .handler = &TEH_handler_reserves_get + }, + { + .op = "history", + .handler = &TEH_handler_reserves_history + }, + { + .op = NULL, + .handler = NULL + }, + }; + + if ( (NULL == args[0]) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &reserve_pub, + sizeof (reserve_pub))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, + args[0]); + } + for (unsigned int i = 0; NULL != h[i].handler; i++) + { + if ( ( (NULL == args[1]) && + (NULL == h[i].op) ) || + ( (NULL != args[1]) && + (NULL != h[i].op) && + (0 == strcmp (h[i].op, + args[1])) ) ) + return h[i].handler (rc, + &reserve_pub); + } + return r404 (rc->connection, + args[1]); +} + + +/** + * Signature of functions that handle operations on purses. + * + * @param connection HTTP request handle + * @param purse_pub the public key of the purse + * @param root uploaded JSON data + * @return MHD result code + */ +typedef MHD_RESULT +(*PurseOpHandler)(struct MHD_Connection *connection, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); + + +/** + * Handle a "/purses/$RESERVE_PUB/$OP" POST request. Parses the "purse_pub" + * EdDSA key of the purse and demultiplexes based on $OP. + * + * @param rc request context + * @param root uploaded JSON data + * @param args array of additional options + * @return MHD result code + */ +static MHD_RESULT +handle_post_purses (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]) +{ + struct TALER_PurseContractPublicKeyP purse_pub; + static const struct + { + /** + * Name of the operation (args[1]) + */ + const char *op; + + /** + * Function to call to perform the operation. + */ + PurseOpHandler handler; + + } h[] = { + { + .op = "create", + .handler = &TEH_handler_purses_create + }, + { + .op = "deposit", + .handler = &TEH_handler_purses_deposit + }, + { + .op = "merge", + .handler = &TEH_handler_purses_merge + }, + { + .op = NULL, + .handler = NULL + }, + }; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &purse_pub, + sizeof (purse_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, + args[0]); + } + for (unsigned int i = 0; NULL != h[i].op; i++) + if (0 == strcmp (h[i].op, + args[1])) + return h[i].handler (rc->connection, + &purse_pub, + root); + return r404 (rc->connection, + args[1]); +} + + +/** * Increments our request counter and checks if this * process should commit suicide. */ @@ -371,6 +1021,11 @@ handle_mhd_completion_callback (void *cls, TEH_check_invariants (); if (NULL != rc->rh_cleaner) rc->rh_cleaner (rc); + if (NULL != rc->root) + { + json_decref (rc->root); + rc->root = NULL; + } TEH_check_invariants (); { #if MHD_VERSION >= 0x00097304 @@ -435,9 +1090,8 @@ proceed_with_handler (struct TEH_RequestContext *rc, size_t *upload_data_size) { const struct TEH_RequestHandler *rh = rc->rh; - const char *args[rh->nargs + 1]; + const char *args[rh->nargs + 2]; size_t ulen = strlen (url) + 1; - json_t *root = NULL; MHD_RESULT ret; /* We do check for "ulen" here, because we'll later stack-allocate a buffer @@ -458,8 +1112,9 @@ proceed_with_handler (struct TEH_RequestContext *rc, /* All POST endpoints come with a body in JSON format. So we parse the JSON here. */ - if (0 == strcasecmp (rh->method, - MHD_HTTP_METHOD_POST)) + if ( (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_POST)) && + (NULL == rc->root) ) { enum GNUNET_GenericReturnValue res; @@ -467,83 +1122,74 @@ proceed_with_handler (struct TEH_RequestContext *rc, &rc->opaque_post_parsing_context, upload_data, upload_data_size, - &root); + &rc->root); if (GNUNET_SYSERR == res) { - GNUNET_assert (NULL == root); + GNUNET_assert (NULL == rc->root); return MHD_NO; /* bad upload, could not even generate error */ } if ( (GNUNET_NO == res) || - (NULL == root) ) + (NULL == rc->root) ) { - GNUNET_assert (NULL == root); + GNUNET_assert (NULL == rc->root); return MHD_YES; /* so far incomplete upload or parser error */ } } { char d[ulen]; - - /* Parse command-line arguments, if applicable */ - args[0] = NULL; - if (rh->nargs > 0) - { - unsigned int i; - const char *fin; - char *sp; - - /* make a copy of 'url' because 'strtok_r()' will modify */ - memcpy (d, - url, - ulen); - i = 0; - args[i++] = strtok_r (d, "/", &sp); - while ( (NULL != args[i - 1]) && - (i < rh->nargs) ) - args[i++] = strtok_r (NULL, "/", &sp); - /* make sure above loop ran nicely until completion, and also - that there is no excess data in 'd' afterwards */ - if ( (! rh->nargs_is_upper_bound) && - ( (i != rh->nargs) || - (NULL == args[i - 1]) || - (NULL != (fin = strtok_r (NULL, "/", &sp))) ) ) - { - char emsg[128 + 512]; - - GNUNET_snprintf (emsg, - sizeof (emsg), - "Got %u/%u segments for %s request ('%s')", - (NULL == args[i - 1]) - ? i - 1 - : i + ((NULL != fin) ? 1 : 0), - rh->nargs, - rh->url, - url); - GNUNET_break_op (0); - json_decref (root); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS, - emsg); - } - - /* just to be safe(r), we always terminate the array with a NULL - (even if handlers requested precise number of arguments) */ - args[i] = NULL; + unsigned int i; + char *sp; + + /* Parse command-line arguments */ + /* make a copy of 'url' because 'strtok_r()' will modify */ + GNUNET_memcpy (d, + url, + ulen); + i = 0; + args[i++] = strtok_r (d, "/", &sp); + while ( (NULL != args[i - 1]) && + (i <= rh->nargs + 1) ) + args[i++] = strtok_r (NULL, "/", &sp); + /* make sure above loop ran nicely until completion, and also + that there is no excess data in 'd' afterwards */ + if ( ( (rh->nargs_is_upper_bound) && + (i - 1 > rh->nargs) ) || + ( (! rh->nargs_is_upper_bound) && + (i - 1 != rh->nargs) ) ) + { + char emsg[128 + 512]; + + GNUNET_snprintf (emsg, + sizeof (emsg), + "Got %u+/%u segments for `%s' request (`%s')", + i - 1, + rh->nargs, + rh->url, + url); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS, + emsg); } + GNUNET_assert (NULL == args[i - 1]); /* Above logic ensures that 'root' is exactly non-NULL for POST operations, so we test for 'root' to decide which handler to invoke. */ - if (NULL != root) + if (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_POST)) ret = rh->handler.post (rc, - root, + rc->root, args); - else /* We also only have "POST" or "GET" in the API for at this point - (OPTIONS/HEAD are taken care of earlier) */ + else if (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_DELETE)) + ret = rh->handler.delete (rc, + args); + else /* Only GET left */ ret = rh->handler.get (rc, args); } - json_decref (root); return ret; } @@ -586,6 +1232,20 @@ handler_seed (struct TEH_RequestContext *rc, /** + * Signature of functions that handle simple + * POST operations for the management API. + * + * @param connection the MHD connection to handle + * @param root uploaded JSON data + * @return MHD result code + */ +typedef MHD_RESULT +(*ManagementPostHandler)( + struct MHD_Connection *connection, + const json_t *root); + + +/** * Handle POST "/management/..." requests. * * @param rc request context @@ -598,6 +1258,55 @@ handle_post_management (struct TEH_RequestContext *rc, const json_t *root, const char *const args[]) { + static const struct + { + const char *arg0; + const char *arg1; + ManagementPostHandler handler; + } plain_posts[] = { + { + .arg0 = "keys", + .handler = &TEH_handler_management_post_keys + }, + { + .arg0 = "wire", + .handler = &TEH_handler_management_post_wire + }, + { + .arg0 = "wire", + .arg1 = "disable", + .handler = &TEH_handler_management_post_wire_disable + }, + { + .arg0 = "wire-fee", + .handler = &TEH_handler_management_post_wire_fees + }, + { + .arg0 = "global-fee", + .handler = &TEH_handler_management_post_global_fees + }, + { + .arg0 = "extensions", + .handler = &TEH_handler_management_post_extensions + }, + { + .arg0 = "drain", + .handler = &TEH_handler_management_post_drain + }, + { + .arg0 = "aml-officers", + .handler = &TEH_handler_management_aml_officers + }, + { + .arg0 = "partners", + .handler = &TEH_handler_management_partners + }, + { + NULL, + NULL, + NULL + } + }; if (NULL == args[0]) { GNUNET_break_op (0); @@ -638,7 +1347,7 @@ handle_post_management (struct TEH_RequestContext *rc, if (0 == strcmp (args[0], "denominations")) { - struct TALER_DenominationHash h_denom_pub; + struct TALER_DenominationHashP h_denom_pub; if ( (NULL == args[0]) || (NULL == args[1]) || @@ -693,46 +1402,22 @@ handle_post_management (struct TEH_RequestContext *rc, &exchange_pub, root); } - if (0 == strcmp (args[0], - "keys")) + for (unsigned int i = 0; + NULL != plain_posts[i].handler; + i++) { - if (NULL != args[1]) + if (0 == strcmp (args[0], + plain_posts[i].arg0)) { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/keys/*"); + if ( ( (NULL == args[1]) && + (NULL == plain_posts[i].arg1) ) || + ( (NULL != args[1]) && + (NULL != plain_posts[i].arg1) && + (0 == strcmp (args[1], + plain_posts[i].arg1)) ) ) + return plain_posts[i].handler (rc->connection, + root); } - return TEH_handler_management_post_keys (rc->connection, - root); - } - if (0 == strcmp (args[0], - "wire")) - { - if (NULL == args[1]) - return TEH_handler_management_post_wire (rc->connection, - root); - if ( (0 != strcmp (args[1], - "disable")) || - (NULL != args[2]) ) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/wire/disable"); - } - return TEH_handler_management_post_wire_disable (rc->connection, - root); - } - if (0 == strcmp (args[0], - "wire-fee")) - { - if (NULL != args[1]) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/wire-fee/*"); - } - return TEH_handler_management_post_wire_fees (rc->connection, - root); } GNUNET_break_op (0); return r404 (rc->connection, @@ -741,7 +1426,7 @@ handle_post_management (struct TEH_RequestContext *rc, /** - * Handle a get "/management" request. + * Handle a GET "/management" request. * * @param rc request context * @param args array of additional options (must be [0] == "keys") @@ -779,7 +1464,7 @@ handle_post_auditors (struct TEH_RequestContext *rc, const char *const args[]) { struct TALER_AuditorPublicKeyP auditor_pub; - struct TALER_DenominationHash h_denom_pub; + struct TALER_DenominationHashP h_denom_pub; if ( (NULL == args[0]) || (NULL == args[1]) || @@ -822,6 +1507,56 @@ handle_post_auditors (struct TEH_RequestContext *rc, /** + * Generates the response for "/", redirecting the + * client to the ``toplevel_redirect_url``. + * + * @param rc request context + * @param args remaining arguments (should be empty) + * @return MHD result code + */ +static MHD_RESULT +toplevel_redirect (struct TEH_RequestContext *rc, + const char *const args[]) +{ + const char *text = "Redirecting to /webui/"; + struct MHD_Response *response; + + response = MHD_create_response_from_buffer (strlen (text), + (void *) text, + MHD_RESPMEM_PERSISTENT); + if (NULL == response) + { + GNUNET_break (0); + return MHD_NO; + } + TALER_MHD_add_global_headers (response); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + if (MHD_NO == + MHD_add_response_header (response, + MHD_HTTP_HEADER_LOCATION, + toplevel_redirect_url)) + { + GNUNET_break (0); + MHD_destroy_response (response); + return MHD_NO; + } + + { + MHD_RESULT ret; + + ret = MHD_queue_response (rc->connection, + MHD_HTTP_FOUND, + response); + MHD_destroy_response (response); + return ret; + } +} + + +/** * Handle incoming HTTP request. * * @param cls closure for MHD daemon (unused) @@ -854,15 +1589,11 @@ handle_mhd_request (void *cls, .data = "User-agent: *\nDisallow: /\n", .response_code = MHD_HTTP_OK }, - /* Landing page, tell humans to go away. */ + /* Landing page, redirect to toplevel_redirect_url */ { .url = "", .method = MHD_HTTP_METHOD_GET, - .handler.get = TEH_handler_static_response, - .mime_type = "text/plain", - .data = - "Hello, I'm the Taler exchange. This HTTP server is not for humans.\n", - .response_code = MHD_HTTP_OK + .handler.get = &toplevel_redirect }, /* AGPL licensing page, redirect to source. As per the AGPL-license, every deployment is required to offer the user a download of the source of @@ -878,6 +1609,12 @@ handle_mhd_request (void *cls, .method = MHD_HTTP_METHOD_GET, .handler.get = &handler_seed }, + /* Configuration */ + { + .url = "config", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &TEH_handler_config + }, /* Performance metrics */ { .url = "metrics", @@ -902,25 +1639,57 @@ handle_mhd_request (void *cls, .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_keys_get_handler, }, - /* Requests for wiring information */ { - .url = "wire", - .method = MHD_HTTP_METHOD_GET, - .handler.get = &TEH_handler_wire + .url = "batch-deposit", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_batch_deposit, + .nargs = 0 + }, + /* request R, used in clause schnorr withdraw and refresh */ + { + .url = "csr-melt", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_csr_melt, + .nargs = 0 + }, + { + .url = "csr-withdraw", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_csr_withdraw, + .nargs = 0 }, /* Withdrawing coins / interaction with reserves */ { .url = "reserves", .method = MHD_HTTP_METHOD_GET, - .handler.get = &TEH_handler_reserves_get, - .nargs = 1 + .handler.get = &handle_get_reserves, + .nargs = 2, + .nargs_is_upper_bound = true }, { .url = "reserves", .method = MHD_HTTP_METHOD_POST, - .handler.post = &TEH_handler_withdraw, + .handler.post = &handle_post_reserves, + .nargs = 2 + }, + { + .url = "age-withdraw", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &handle_post_age_withdraw, .nargs = 2 }, + { + .url = "reserves-attest", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &TEH_handler_reserves_get_attest, + .nargs = 1 + }, + { + .url = "reserves-attest", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_reserves_attest, + .nargs = 1 + }, /* coins */ { .url = "coins", @@ -931,8 +1700,9 @@ handle_mhd_request (void *cls, { .url = "coins", .method = MHD_HTTP_METHOD_GET, - .handler.get = TEH_handler_link, + .handler.get = &handle_get_coins, .nargs = 2, + .nargs_is_upper_bound = true }, /* refreshes/$RCH/reveal */ { @@ -955,12 +1725,40 @@ handle_mhd_request (void *cls, .handler.get = &TEH_handler_deposits_get, .nargs = 4 }, + /* Operating on purses */ + { + .url = "purses", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &handle_post_purses, + .nargs = 2 + }, + /* Getting purse status */ + { + .url = "purses", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &TEH_handler_purses_get, + .nargs = 2 + }, + /* Deleting purse */ + { + .url = "purses", + .method = MHD_HTTP_METHOD_DELETE, + .handler.delete = &TEH_handler_purses_delete, + .nargs = 1 + }, + /* Getting contracts */ + { + .url = "contracts", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &TEH_handler_contracts_get, + .nargs = 1 + }, /* KYC endpoints */ { .url = "kyc-check", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_kyc_check, - .nargs = 1 + .nargs = 3 }, { .url = "kyc-proof", @@ -974,6 +1772,20 @@ handle_mhd_request (void *cls, .handler.post = &TEH_handler_kyc_wallet, .nargs = 0 }, + { + .url = "kyc-webhook", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &TEH_handler_kyc_webhook_get, + .nargs = 16, /* more is not plausible */ + .nargs_is_upper_bound = true + }, + { + .url = "kyc-webhook", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_kyc_webhook_post, + .nargs = 16, /* more is not plausible */ + .nargs_is_upper_bound = true + }, /* POST management endpoints */ { .url = "management", @@ -997,6 +1809,28 @@ handle_mhd_request (void *cls, .nargs = 4, .nargs_is_upper_bound = true }, + /* AML endpoints */ + { + .url = "aml", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &handle_get_aml, + .nargs = 4, + .nargs_is_upper_bound = true + }, + { + .url = "aml", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &handle_post_aml, + .nargs = 2 + }, + { + .url = "webui", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &TEH_handler_spa, + .nargs = 1, + .nargs_is_upper_bound = true + }, + /* mark end of list */ { .url = NULL @@ -1037,30 +1871,8 @@ handle_mhd_request (void *cls, if (0 == strcasecmp (method, MHD_HTTP_METHOD_POST)) { - const char *cl; - - /* Maybe check for maximum upload size - and refuse requests if they are just too big. */ - cl = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_CONTENT_LENGTH); - if (NULL != cl) - { - unsigned long long cv; - char dummy; - - if (1 != sscanf (cl, - "%llu%c", - &cv, - &dummy)) - { - /* Not valid HTTP request, just close connection. */ - GNUNET_break_op (0); - return MHD_NO; - } - if (cv > TALER_MHD_REQUEST_BUFFER_MAX) - return TALER_MHD_reply_request_too_large (connection); - } + TALER_MHD_check_content_length (connection, + TALER_MHD_REQUEST_BUFFER_MAX); } } @@ -1140,7 +1952,8 @@ handle_mhd_request (void *cls, continue; found = true; /* The URL is a match! What we now do depends on the method. */ - if (0 == strcasecmp (method, MHD_HTTP_METHOD_OPTIONS)) + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_OPTIONS)) { GNUNET_async_scope_restore (&old_scope); return TALER_MHD_reply_cors_preflight (connection); @@ -1238,248 +2051,178 @@ handle_mhd_request (void *cls, /** - * Load general KYC configuration parameters for the exchange server into the - * #TEH_kyc_config variable. + * Load configuration parameters for the exchange + * server into the corresponding global variables. * * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue -parse_kyc_settings (void) +exchange_serve_process_config (void) { + static struct TALER_CurrencySpecification defspec = { + .num_fractional_input_digits = 2, + .num_fractional_normal_digits = 2, + .num_fractional_trailing_zero_digits = 2 + }; if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (TEH_cfg, - "exchange", - "KYC_WITHDRAW_PERIOD", - &TEH_kyc_config.withdraw_period)) + TALER_KYCLOGIC_kyc_init (TEH_cfg)) { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "exchange", - "KYC_WITHDRAW_PERIOD", - "valid relative time expected"); return GNUNET_SYSERR; } - if (GNUNET_TIME_relative_is_zero (TEH_kyc_config.withdraw_period)) - return GNUNET_OK; if (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, - "exchange", - "KYC_WITHDRAW_LIMIT", - &TEH_kyc_config.withdraw_limit)) - return GNUNET_SYSERR; - if (0 != strcasecmp (TEH_kyc_config.withdraw_limit.currency, - TEH_currency)) + GNUNET_CONFIGURATION_get_value_number (TEH_cfg, + "exchange", + "MAX_REQUESTS", + &req_max)) { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "exchange", - "KYC_WITHDRAW_LIMIT", - "currency mismatch"); - return GNUNET_SYSERR; + req_max = ULLONG_MAX; } - return GNUNET_OK; -} - - -/** - * Load OAuth2.0 configuration parameters for the exchange server into the - * #TEH_kyc_config variable. - * - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -parse_kyc_oauth_cfg (void) -{ - char *s; - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (TEH_cfg, - "exchange-kyc-oauth2", - "KYC_OAUTH2_URL", - &s)) + GNUNET_CONFIGURATION_get_value_time (TEH_cfg, + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME", + &TEH_reserve_closing_delay)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange-kyc-oauth2", - "KYC_OAUTH2_URL"); - return GNUNET_SYSERR; - } - if ( (! TALER_url_valid_charset (s)) || - ( (0 != strncasecmp (s, - "http://", - strlen ("http://"))) && - (0 != strncasecmp (s, - "https://", - strlen ("https://"))) ) ) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "exchange-kyc-oauth2", - "KYC_OAUTH2_URL", - "not a valid URL"); - GNUNET_free (s); - return GNUNET_SYSERR; + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME"); + /* use default */ + TEH_reserve_closing_delay + = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_WEEKS, + 4); } - TEH_kyc_config.details.oauth2.url = s; if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (TEH_cfg, - "exchange-kyc-oauth2", - "KYC_INFO_URL", - &s)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange-kyc-oauth2", - "KYC_INFO_URL"); - return GNUNET_SYSERR; - } - if ( (! TALER_url_valid_charset (s)) || - ( (0 != strncasecmp (s, - "http://", - strlen ("http://"))) && - (0 != strncasecmp (s, - "https://", - strlen ("https://"))) ) ) + GNUNET_CONFIGURATION_get_value_time (TEH_cfg, + "exchange", + "MAX_KEYS_CACHING", + &TEH_max_keys_caching)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "exchange-kyc-oauth2", - "KYC_INFO_URL", - "not a valid URL"); - GNUNET_free (s); + "exchange", + "MAX_KEYS_CACHING", + "valid relative time expected"); return GNUNET_SYSERR; } - TEH_kyc_config.details.oauth2.info_url = s; - if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (TEH_cfg, - "exchange-kyc-oauth2", - "KYC_OAUTH2_CLIENT_ID", - &s)) + "exchange", + "KYC_AML_TRIGGER", + &TEH_kyc_aml_trigger)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange-kyc-oauth2", - "KYC_OAUTH2_CLIENT_ID"); + "exchange", + "KYC_AML_TRIGGER"); return GNUNET_SYSERR; } - TEH_kyc_config.details.oauth2.client_id = s; - if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (TEH_cfg, - "exchange-kyc-oauth2", - "KYC_OAUTH2_CLIENT_SECRET", - &s)) + "exchange", + "TOPLEVEL_REDIRECT_URL", + &toplevel_redirect_url)) { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange-kyc-oauth2", - "KYC_OAUTH2_CLIENT_SECRET"); - return GNUNET_SYSERR; + toplevel_redirect_url = GNUNET_strdup ("/terms"); } - TEH_kyc_config.details.oauth2.client_secret = s; - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (TEH_cfg, - "exchange-kyc-oauth2", - "KYC_OAUTH2_POST_URL", - &s)) + TALER_config_get_currency (TEH_cfg, + &TEH_currency)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange-kyc-oauth2", - "KYC_OAUTH2_POST_URL"); + "taler", + "CURRENCY"); return GNUNET_SYSERR; } - TEH_kyc_config.details.oauth2.post_kyc_redirect_url = s; - return GNUNET_OK; -} - -/** - * Load configuration parameters for the exchange - * server into the corresponding global variables. - * - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -exchange_serve_process_config (void) -{ + if (GNUNET_OK != + TALER_CONFIG_parse_currencies (TEH_cfg, + &num_cspecs, + &cspecs)) + return GNUNET_SYSERR; + for (unsigned int i = 0; i<num_cspecs; i++) { - char *kyc_mode; + struct TALER_CurrencySpecification *cspec; - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (TEH_cfg, - "exchange", - "KYC_MODE", - &kyc_mode)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange", - "KYC_MODE"); - return GNUNET_SYSERR; - } - if (0 == strcasecmp (kyc_mode, - "NONE")) - { - TEH_kyc_config.mode = TEH_KYC_NONE; - } - else if (0 == strcasecmp (kyc_mode, - "OAUTH2")) - { - TEH_kyc_config.mode = TEH_KYC_OAUTH2; - if (GNUNET_OK != - parse_kyc_oauth_cfg ()) - { - GNUNET_free (kyc_mode); - return GNUNET_SYSERR; - } - } - else + cspec = &cspecs[i]; + if (0 == strcmp (TEH_currency, + cspec->currency)) { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "exchange", - "KYC_MODE", - "Must be 'NONE' or 'OAUTH2'"); - GNUNET_free (kyc_mode); - return GNUNET_SYSERR; + TEH_cspec = cspec; + break; } - GNUNET_free (kyc_mode); } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_number (TEH_cfg, - "exchange", - "MAX_REQUESTS", - &req_max)) + if (NULL == TEH_cspec) { - req_max = ULLONG_MAX; + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, + "taler", + "CURRENCY", + "Lacking enabled currency specification for the given currency, using default"); + defspec.map_alt_unit_names + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("0", + TEH_currency) + ); + defspec.name = TEH_currency; + GNUNET_assert (strlen (TEH_currency) < + sizeof (defspec.currency)); + strcpy (defspec.currency, + TEH_currency); + TEH_cspec = &defspec; } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (TEH_cfg, - "exchangedb", - "IDLE_RESERVE_EXPIRATION_TIME", - &TEH_reserve_closing_delay)) + TALER_config_get_amount (TEH_cfg, + "exchange", + "AML_THRESHOLD", + &TEH_aml_threshold)) { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchangedb", - "IDLE_RESERVE_EXPIRATION_TIME"); - /* use default */ - TEH_reserve_closing_delay - = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_WEEKS, - 4); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Need amount in section `exchange' under `AML_THRESHOLD'\n"); + return GNUNET_SYSERR; } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (TEH_cfg, - "exchange", - "MAX_KEYS_CACHING", - &TEH_max_keys_caching)) + TALER_config_get_amount (TEH_cfg, + "exchange", + "STEFAN_ABS", + &TEH_stefan_abs)) { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &TEH_stefan_abs)); + } + if (GNUNET_OK != + TALER_config_get_amount (TEH_cfg, "exchange", - "MAX_KEYS_CACHING", - "valid relative time expected"); - return GNUNET_SYSERR; + "STEFAN_LOG", + &TEH_stefan_log)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &TEH_stefan_log)); } if (GNUNET_OK != - TALER_config_get_currency (TEH_cfg, - &TEH_currency)) + GNUNET_CONFIGURATION_get_value_float (TEH_cfg, + "exchange", + "STEFAN_LIN", + &TEH_stefan_lin)) { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "taler", - "CURRENCY"); + TEH_stefan_lin = 0.0f; + } + + if (0 != strcmp (TEH_currency, + TEH_aml_threshold.currency)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Amount in section `exchange' under `AML_THRESHOLD' uses the wrong currency!\n"); + return GNUNET_SYSERR; + } + TEH_enable_rewards + = GNUNET_CONFIGURATION_get_value_yesno ( + TEH_cfg, + "exchange", + "ENABLE_REWARDS"); + if (GNUNET_SYSERR == TEH_enable_rewards) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Need YES or NO in section `exchange' under `ENABLE_REWARDS'\n"); return GNUNET_SYSERR; } if (GNUNET_OK != @@ -1502,35 +2245,6 @@ exchange_serve_process_config (void) return GNUNET_SYSERR; } - if (TEH_KYC_NONE != TEH_kyc_config.mode) - { - if (GNUNET_YES == - GNUNET_CONFIGURATION_have_value (TEH_cfg, - "exchange", - "KYC_WALLET_BALANCE_LIMIT")) - { - if ( (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, - "exchange", - "KYC_WALLET_BALANCE_LIMIT", - &TEH_kyc_config.wallet_balance_limit)) || - (0 != strcasecmp (TEH_currency, - TEH_kyc_config.wallet_balance_limit.currency)) ) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "exchange", - "KYC_WALLET_BALANCE_LIMIT", - "valid amount expected"); - return GNUNET_SYSERR; - } - } - else - { - memset (&TEH_kyc_config.wallet_balance_limit, - 0, - sizeof (TEH_kyc_config.wallet_balance_limit)); - } - } { char *master_public_key_str; @@ -1546,34 +2260,58 @@ exchange_serve_process_config (void) return GNUNET_SYSERR; } if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_public_key_from_string (master_public_key_str, - strlen ( - master_public_key_str), - &TEH_master_public_key. - eddsa_pub)) + GNUNET_CRYPTO_eddsa_public_key_from_string ( + master_public_key_str, + strlen (master_public_key_str), + &TEH_master_public_key.eddsa_pub)) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Invalid master public key given in exchange configuration."); + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "MASTER_PUBLIC_KEY", + "invalid base32 encoding for a master public key"); GNUNET_free (master_public_key_str); return GNUNET_SYSERR; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Launching exchange with public key `%s'...\n", + master_public_key_str); GNUNET_free (master_public_key_str); } - if (TEH_KYC_NONE != TEH_kyc_config.mode) + { + char *attr_enc_key_str; + if (GNUNET_OK != - parse_kyc_settings ()) + GNUNET_CONFIGURATION_get_value_string (TEH_cfg, + "exchange", + "ATTRIBUTE_ENCRYPTION_KEY", + &attr_enc_key_str)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "ATTRIBUTE_ENCRYPTION_KEY"); return GNUNET_SYSERR; + } + GNUNET_CRYPTO_hash (attr_enc_key_str, + strlen (attr_enc_key_str), + &TEH_attribute_key.hash); + GNUNET_free (attr_enc_key_str); } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Launching exchange with public key `%s'...\n", - GNUNET_p2s (&TEH_master_public_key.eddsa_pub)); - if (NULL == - (TEH_plugin = TALER_EXCHANGEDB_plugin_load (TEH_cfg))) + for (unsigned int i = 0; i<MAX_DB_RETRIES; i++) + { + TEH_plugin = TALER_EXCHANGEDB_plugin_load (TEH_cfg); + if (NULL != TEH_plugin) + break; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to connect to DB, will try again %u times\n", + MAX_DB_RETRIES - i); + sleep (1); + } + if (NULL == TEH_plugin) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to initialize DB subsystem\n"); + "Failed to initialize DB subsystem. Giving up.\n"); return GNUNET_SYSERR; } return GNUNET_OK; @@ -1718,7 +2456,9 @@ run_single_request (void) xfork = fork (); if (-1 == xfork) { - global_ret = EXIT_FAILURE; + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "fork"); + global_ret = EXIT_NO_RESTART; GNUNET_SCHEDULER_shutdown (); return; } @@ -1805,11 +2545,17 @@ do_shutdown (void *cls) mhd = TALER_MHD_daemon_stop (); TEH_resume_keys_requests (true); + TEH_deposits_get_cleanup (); TEH_reserves_get_cleanup (); + TEH_purses_get_cleanup (); TEH_kyc_check_cleanup (); TEH_kyc_proof_cleanup (); + TALER_KYCLOGIC_kyc_done (); if (NULL != mhd) + { MHD_stop_daemon (mhd); + mhd = NULL; + } TEH_wire_done (); TEH_extensions_done (); TEH_keys_finished (); @@ -1828,6 +2574,12 @@ do_shutdown (void *cls) GNUNET_CURL_gnunet_rc_destroy (exchange_curl_rc); exchange_curl_rc = NULL; } + TALER_TEMPLATING_done (); + TEH_cspec = NULL; + TALER_CONFIG_free_currencies (num_cspecs, + cspecs); + num_cspecs = 0; + cspecs = NULL; } @@ -1865,31 +2617,48 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } + if (GNUNET_OK != + TEH_spa_init ()) + { + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_TEMPLATING_init ("exchange")) + { + global_ret = EXIT_NOTINSTALLED; + GNUNET_SCHEDULER_shutdown (); + return; + } if (GNUNET_SYSERR == TEH_plugin->preflight (TEH_plugin->cls)) { - global_ret = EXIT_FAILURE; + GNUNET_break (0); + global_ret = EXIT_NO_RESTART; GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != TEH_extensions_init ()) { - global_ret = EXIT_FAILURE; + global_ret = EXIT_NOTINSTALLED; GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != TEH_keys_init ()) { - global_ret = EXIT_FAILURE; + GNUNET_break (0); + global_ret = EXIT_NO_RESTART; GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != TEH_wire_init ()) { - global_ret = EXIT_FAILURE; + GNUNET_break (0); + global_ret = EXIT_NO_RESTART; GNUNET_SCHEDULER_shutdown (); return; } @@ -1901,7 +2670,7 @@ run (void *cls, if (NULL == TEH_curl_ctx) { GNUNET_break (0); - global_ret = EXIT_FAILURE; + global_ret = EXIT_NO_RESTART; GNUNET_SCHEDULER_shutdown (); return; } @@ -1940,8 +2709,8 @@ run (void *cls, MHD_OPTION_CONNECTION_TIMEOUT, connection_timeout, (0 == allow_address_reuse) - ? MHD_OPTION_END - : MHD_OPTION_LISTENING_ADDRESS_REUSE, + ? MHD_OPTION_END + : MHD_OPTION_LISTENING_ADDRESS_REUSE, (unsigned int) allow_address_reuse, MHD_OPTION_END); if (NULL == mhd) @@ -1954,7 +2723,6 @@ run (void *cls, global_ret = EXIT_SUCCESS; TALER_MHD_daemon_start (mhd); atexit (&write_stats); - #if HAVE_DEVELOPER if (NULL != input_filename) run_single_request (); diff --git a/src/exchange/taler-exchange-httpd.h b/src/exchange/taler-exchange-httpd.h index 4f04029e6..25e9e1105 100644 --- a/src/exchange/taler-exchange-httpd.h +++ b/src/exchange/taler-exchange-httpd.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014, 2015, 2020 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 Affero General Public License as published by the Free Software @@ -25,107 +25,13 @@ #include <microhttpd.h> #include "taler_json_lib.h" -#include "taler_crypto_lib.h" +#include "taler_util.h" +#include "taler_kyclogic_plugin.h" #include "taler_extensions.h" #include <gnunet/gnunet_mhd_compat.h> /** - * Enumeration for our KYC modes. - */ -enum TEH_KycMode -{ - /** - * KYC is disabled. - */ - TEH_KYC_NONE = 0, - - /** - * We use Oauth2.0. - */ - TEH_KYC_OAUTH2 = 1 -}; - - -/** - * Structure describing our KYC configuration. - */ -struct TEH_KycOptions -{ - /** - * What KYC mode are we in? - */ - enum TEH_KycMode mode; - - /** - * Maximum amount that can be withdrawn in @e withdraw_period without - * needing KYC. - * Only valid if @e mode is not #TEH_KYC_NONE and - * if @e withdraw_period is non-zero. - */ - struct TALER_Amount withdraw_limit; - - /** - * Maximum balance a wallet can hold without - * needing KYC. - * Only valid if @e mode is not #TEH_KYC_NONE and - * if the amount specified is valid. - */ - struct TALER_Amount wallet_balance_limit; - - /** - * Time period over which @e withdraw_limit applies. - * Only valid if @e mode is not #TEH_KYC_NONE. - */ - struct GNUNET_TIME_Relative withdraw_period; - - /** - * Details depending on @e mode. - */ - union - { - - /** - * Configuration details if @e mode is #TEH_KYC_OAUTH2. - */ - struct - { - - /** - * URL of the OAuth2.0 endpoint for KYC checks. - */ - char *url; - - /** - * URL of the user info access endpoint. - */ - char *info_url; - - /** - * Our client ID for OAuth2.0. - */ - char *client_id; - - /** - * Our client secret for OAuth2.0. - */ - char *client_secret; - - /** - * Where to redirect clients after the - * Web-based KYC process is done? - */ - char *post_kyc_redirect_url; - - } oauth2; - - } details; -}; - - -extern struct TEH_KycOptions TEH_kyc_config; - -/** * How long is caching /keys allowed at most? */ extern struct GNUNET_TIME_Relative TEH_max_keys_caching; @@ -159,6 +65,11 @@ extern int TEH_check_invariants_flag; extern int TEH_allow_keys_timetravel; /** + * Option set to #GNUNET_YES if rewards are allowed. + */ +extern int TEH_enable_rewards; + +/** * Main directory with revocation data. */ extern char *TEH_revocation_directory; @@ -177,16 +88,53 @@ extern bool TEH_suicide; extern struct TALER_MasterPublicKeyP TEH_master_public_key; /** + * Key used to encrypt KYC attribute data in our database. + */ +extern struct TALER_AttributeEncryptionKeyP TEH_attribute_key; + +/** * Our DB plugin. */ extern struct TALER_EXCHANGEDB_Plugin *TEH_plugin; /** + * Absolute STEFAN parameter. + */ +extern struct TALER_Amount TEH_stefan_abs; + +/** + * Logarithmic STEFAN parameter. + */ +extern struct TALER_Amount TEH_stefan_log; + +/** + * Linear STEFAN parameter. + */ +extern float TEH_stefan_lin; + +/** + * Default ways how to render #TEH_currency amounts. + */ +extern const struct TALER_CurrencySpecification *TEH_cspec; + +/** * Our currency. */ extern char *TEH_currency; /** + * Name of the KYC-AML-trigger evaluation binary. + */ +extern char *TEH_kyc_aml_trigger; + +/** + * What is the largest amount we allow a peer to + * merge into a reserve before always triggering + * an AML check? + */ +extern struct TALER_Amount TEH_aml_threshold; + +/** * Our (externally visible) base URL. */ extern char *TEH_base_url; @@ -201,13 +149,11 @@ extern volatile bool MHD_terminating; */ extern struct GNUNET_CURL_Context *TEH_curl_ctx; -/** - * The manifest of the available extensions, NULL terminated +/* + * Signature of the offline master key of all enabled extensions' configuration */ -extern struct TALER_Extension **TEH_extensions; - -#define TEH_extension_enabled(ext) (0 <= ext && TALER_Extension_Max > ext && \ - NULL != TEH_extensions[ext]->config) +extern struct TALER_MasterSignatureP TEH_extensions_sig; +extern bool TEH_extensions_signed; /** * @brief Struct describing an URL and the handler for it. @@ -253,6 +199,11 @@ struct TEH_RequestContext struct MHD_Connection *connection; /** + * JSON root of uploaded data (or NULL, if none). + */ + json_t *root; + + /** * @e rh-specific cleanup routine. Function called * upon completion of the request that should * clean up @a rh_ctx. Can be NULL. @@ -292,11 +243,10 @@ struct TEH_RequestHandler union { /** - * Function to call to handle a GET requests (and those + * Function to call to handle GET requests (and those * with @e method NULL). * * @param rc context for the request - * @param mime_type the @e mime_type for the reply (hint, can be NULL) * @param args array of arguments, needs to be of length @e args_expected * @return MHD result code */ @@ -306,11 +256,11 @@ struct TEH_RequestHandler /** - * Function to call to handle a POST request. + * Function to call to handle POST requests. * * @param rc context for the request * @param json uploaded JSON data - * @param args array of arguments, needs to be of length @e args_expected + * @param args array of arguments, needs to be of length @e nargs * @return MHD result code */ MHD_RESULT @@ -318,18 +268,18 @@ struct TEH_RequestHandler const json_t *root, const char *const args[]); - } handler; - - /** - * Number of arguments this handler expects in the @a args array. - */ - unsigned int nargs; + /** + * Function to call to handle DELETE requests. + * + * @param rc context for the request + * @param args array of arguments, needs to be of length @e nargs + * @return MHD result code + */ + MHD_RESULT + (*delete)(struct TEH_RequestContext *rc, + const char *const args[]); - /** - * Is the number of arguments given in @e nargs only an upper bound, - * and calling with fewer arguments could be OK? - */ - bool nargs_is_upper_bound; + } handler; /** * Mime type to use in reply (hint, can be NULL). @@ -350,7 +300,22 @@ struct TEH_RequestHandler * Default response code. 0 for none provided. */ unsigned int response_code; + + /** + * Number of arguments this handler expects in the @a args array. + */ + unsigned int nargs; + + /** + * Is the number of arguments given in @e nargs only an upper bound, + * and calling with fewer arguments could be OK? + */ + bool nargs_is_upper_bound; }; +/* Age restriction configuration */ +extern bool TEH_age_restriction_enabled; +extern struct TALER_AgeRestrictionConfig TEH_age_restriction_config; + #endif diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.c b/src/exchange/taler-exchange-httpd_age-withdraw.c new file mode 100644 index 000000000..9276fb191 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_age-withdraw.c @@ -0,0 +1,1019 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General + Public License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_age-withdraw.c + * @brief Handle /reserves/$RESERVE_PUB/age-withdraw requests + * @author Özgür Kesim + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" +#include "taler_error_codes.h" +#include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_age-withdraw.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" +#include "taler_util.h" + + +/** + * Context for #age_withdraw_transaction. + */ +struct AgeWithdrawContext +{ + /** + * KYC status for the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Timestamp + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Hash of the wire source URL, needed when kyc is needed. + */ + struct TALER_PaytoHashP h_payto; + + /** + * The data from the age-withdraw request, as we persist it + */ + struct TALER_EXCHANGEDB_AgeWithdraw commitment; + + /** + * Number of coins/denonations in the reveal + */ + uint32_t num_coins; + + /** + * #num_coins * #kappa hashes of blinded coin planchets. + */ + struct TALER_BlindedPlanchet (*coin_evs) [ TALER_CNC_KAPPA]; + + /** + * #num_coins hashes of the denominations from which the coins are withdrawn. + * Those must support age restriction. + */ + struct TALER_DenominationHashP *denom_hs; + +}; + +/* + * @brief Free the resources within a AgeWithdrawContext + * + * @param awc the context to free + */ +static void +free_age_withdraw_context_resources (struct AgeWithdrawContext *awc) +{ + GNUNET_free (awc->denom_hs); + GNUNET_free (awc->coin_evs); + GNUNET_free (awc->commitment.denom_serials); + /* + * Note: + * awc->commitment.denom_sigs and .h_coin_evs were stack allocated and + * .denom_pub_hashes is NULL for this context. + */ +} + + +/** + * Parse the denominations and blinded coin data of an '/age-withdraw' request. + * + * @param connection The MHD connection to handle + * @param j_denom_hs Array of n hashes of the denominations for the withdrawal, in JSON format + * @param j_blinded_coin_evs Array of n arrays of kappa blinded envelopes of in JSON format for the coins. + * @param[out] awc The context of the operation, only partially built at call time + * @param[out] mhd_ret The result if a reply is queued for MHD + * @return true on success, false on failure, with a reply already queued for MHD + */ +static enum GNUNET_GenericReturnValue +parse_age_withdraw_json ( + struct MHD_Connection *connection, + const json_t *j_denom_hs, + const json_t *j_blinded_coin_evs, + struct AgeWithdrawContext *awc, + MHD_RESULT *mhd_ret) +{ + char buf[256] = {0}; + const char *error = NULL; + unsigned int idx = 0; + json_t *value = NULL; + struct GNUNET_HashContext *hash_context; + + + /* The age value MUST be on the beginning of an age group */ + if (awc->commitment.max_age != + TALER_get_lowest_age (&TEH_age_restriction_config.mask, + awc->commitment.max_age)) + { + error = "max_age must be the lower edge of an age group"; + goto EXIT; + } + + /* Verify JSON-structure consistency */ + { + uint32_t num_coins = json_array_size (j_denom_hs); + + if (! json_is_array (j_denom_hs)) + error = "denoms_h must be an array"; + else if (! json_is_array (j_blinded_coin_evs)) + error = "coin_evs must be an array"; + else if (num_coins == 0) + error = "denoms_h must not be empty"; + else if (num_coins != json_array_size (j_blinded_coin_evs)) + error = "denoms_h and coins_evs must be arrays of the same size"; + else if (num_coins > TALER_MAX_FRESH_COINS) + /** + * The wallet had committed to more than the maximum coins allowed, the + * reserve has been charged, but now the user can not withdraw any money + * from it. Note that the user can't get their money back in this case! + **/ + error = "maximum number of coins that can be withdrawn has been exceeded"; + + _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA), + "TALER_MAX_FRESH_COINS too large"); + + if (NULL != error) + goto EXIT; + + awc->num_coins = num_coins; + awc->commitment.num_coins = num_coins; + } + + /* Continue parsing the parts */ + + /* Parse denomination keys */ + awc->denom_hs = GNUNET_new_array (awc->num_coins, + struct TALER_DenominationHashP); + + json_array_foreach (j_denom_hs, idx, value) { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, &awc->denom_hs[idx]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, spec, NULL, NULL)) + { + GNUNET_snprintf (buf, + sizeof(buf), + "couldn't parse entry no. %d in array denoms_h", + idx + 1); + error = buf; + goto EXIT; + } + }; + + { + typedef struct TALER_BlindedPlanchet + _array_of_kappa_planchets[TALER_CNC_KAPPA]; + + awc->coin_evs = GNUNET_new_array (awc->num_coins, + _array_of_kappa_planchets); + } + + hash_context = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != hash_context); + + /* Parse blinded envelopes. */ + json_array_foreach (j_blinded_coin_evs, idx, value) { + const json_t *j_kappa_coin_evs = value; + if (! json_is_array (j_kappa_coin_evs)) + { + GNUNET_snprintf (buf, + sizeof(buf), + "enxtry %d in array blinded_coin_evs is not an array", + idx + 1); + error = buf; + goto EXIT; + } + else if (TALER_CNC_KAPPA != json_array_size (j_kappa_coin_evs)) + { + GNUNET_snprintf (buf, + sizeof(buf), + "array no. %d in coin_evs not of correct size", + idx + 1); + error = buf; + goto EXIT; + } + + /* Now parse the individual kappa envelopes and calculate the hash of + * the commitment along the way. */ + { + unsigned int kappa = 0; + + json_array_foreach (j_kappa_coin_evs, kappa, value) { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_blinded_planchet (NULL, + &awc->coin_evs[idx][kappa]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, + NULL)) + { + GNUNET_snprintf (buf, + sizeof(buf), + "couldn't parse array no. %d in blinded_coin_evs[%d]", + kappa + 1, + idx + 1); + error = buf; + goto EXIT; + } + + /* Continue to hash of the coin candidates */ + { + struct TALER_BlindedCoinHashP bch; + + TALER_coin_ev_hash (&awc->coin_evs[idx][kappa], + &awc->denom_hs[idx], + &bch); + GNUNET_CRYPTO_hash_context_read (hash_context, + &bch, + sizeof(bch)); + } + + /* Check for duplicate planchets. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol */ + for (unsigned int i = 0; i < idx; i++) + { + if (0 == TALER_blinded_planchet_cmp (&awc->coin_evs[idx][kappa], + &awc->coin_evs[i][kappa])) + { + GNUNET_JSON_parse_free (spec); + error = "duplicate planchet"; + goto EXIT; + } + } + } + } + }; /* json_array_foreach over j_blinded_coin_evs */ + + /* Finally, calculate the h_commitment from all blinded envelopes */ + GNUNET_CRYPTO_hash_context_finish (hash_context, + &awc->commitment.h_commitment.hash); + + GNUNET_assert (NULL == error); + + +EXIT: + if (NULL != error) + { + /* Note: resources are freed in caller */ + + *mhd_ret = TALER_MHD_reply_with_ec ( + connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + error); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * Check if the given denomination is still or already valid, has not been + * revoked and supports age restriction. + * + * @param connection HTTP-connection to the client + * @param ksh The handle to the current state of (denomination) keys in the exchange + * @param denom_h Hash of the denomination key to check + * @param[out] pdk On success, will contain the denomination key details + * @param[out] result On failure, an MHD-response will be queued and result will be set to accordingly + * @return true on success (denomination valid), false otherwise + */ +static bool +denomination_is_valid ( + struct MHD_Connection *connection, + struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *denom_h, + struct TEH_DenominationKey **pdk, + MHD_RESULT *result) +{ + struct TEH_DenominationKey *dk; + dk = TEH_keys_denomination_by_hash_from_state (ksh, + denom_h, + connection, + result); + if (NULL == dk) + { + /* The denomination doesn't exist */ + /* Note: a HTTP-response has been queued and result has been set by + * TEH_keys_denominations_by_hash_from_state */ + return false; + } + + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) + { + /* This denomination is past the expiration time for withdraws */ + /* FIXME[oec]: add idempotency check */ + *result = TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "age-withdraw_reveal"); + return false; + } + + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + *result = TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "age-withdraw_reveal"); + return false; + } + + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + *result = TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + NULL); + return false; + } + + if (0 == dk->denom_pub.age_mask.bits) + { + /* This denomation does not support age restriction */ + char msg[256] = {0}; + GNUNET_snprintf (msg, + sizeof(msg), + "denomination %s does not support age restriction", + GNUNET_h2s (&denom_h->hash)); + + *result = TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + msg); + return false; + } + + *pdk = dk; + return true; +} + + +/** + * Check if the given array of hashes of denomination_keys a) belong + * to valid denominations and b) those are marked as age restricted. + * Also, calculate the total amount of the denominations including fees + * for withdraw. + * + * @param connection The HTTP connection to the client + * @param len The lengths of the array @a denoms_h + * @param denom_hs array of hashes of denomination public keys + * @param coin_evs array of blinded coin planchet candidates + * @param[out] denom_serials On success, will be filled with the serial-id's of the denomination keys. Caller must deallocate. + * @param[out] amount_with_fee On success, will contain the committed amount including fees + * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. + * @return #GNUNET_OK if the denominations are valid and support age-restriction + * #GNUNET_SYSERR otherwise + */ +static enum GNUNET_GenericReturnValue +are_denominations_valid ( + struct MHD_Connection *connection, + uint32_t len, + const struct TALER_DenominationHashP *denom_hs, + const struct TALER_BlindedPlanchet (*coin_evs) [ TALER_CNC_KAPPA], + uint64_t **denom_serials, + struct TALER_Amount *amount_with_fee, + MHD_RESULT *result) +{ + struct TALER_Amount total_amount; + struct TALER_Amount total_fee; + struct TEH_KeyStateHandle *ksh; + uint64_t *serials; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + *denom_serials = + serials = GNUNET_new_array (len, uint64_t); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_fee)); + + for (uint32_t i = 0; i < len; i++) + { + struct TEH_DenominationKey *dk; + if (! denomination_is_valid (connection, + ksh, + &denom_hs[i], + &dk, + result)) + /* FIXME[oec]: add idempotency check */ + return GNUNET_SYSERR; + + /* Ensure the ciphers from the planchets match the denominations' */ + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + if (dk->denom_pub.bsign_pub_key->cipher != + coin_evs[i][k].blinded_message->cipher) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL); + return GNUNET_SYSERR; + } + } + + /* Accumulate the values */ + if (0 > TALER_amount_add (&total_amount, + &total_amount, + &dk->meta.value)) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, + "amount"); + return GNUNET_SYSERR; + } + + /* Accumulate the withdraw fees */ + if (0 > TALER_amount_add (&total_fee, + &total_fee, + &dk->meta.fees.withdraw)) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, + "fee"); + return GNUNET_SYSERR; + } + + serials[i] = dk->meta.serial; + } + + /* Save the total amount including fees */ + GNUNET_assert (0 < TALER_amount_add (amount_with_fee, + &total_amount, + &total_fee)); + + return GNUNET_OK; +} + + +/** + * @brief Verify the signature of the request body with the reserve key + * + * @param connection the connection to the client + * @param commitment the age withdraw commitment + * @param mhd_ret the response to fill in the error case + * @return GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +verify_reserve_signature ( + struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, + enum MHD_Result *mhd_ret) +{ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_age_withdraw_verify (&commitment->h_commitment, + &commitment->amount_with_fee, + &TEH_age_restriction_config.mask, + commitment->max_age, + &commitment->reserve_pub, + &commitment->reserve_sig)) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, + NULL); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * Send a response to a "age-withdraw" request. + * + * @param connection the connection to send the response to + * @param ach value the client committed to + * @param noreveal_index which index will the client not have to reveal + * @return a MHD status code + */ +static MHD_RESULT +reply_age_withdraw_success ( + struct MHD_Connection *connection, + const struct TALER_AgeWithdrawCommitmentHashP *ach, + uint32_t noreveal_index) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec = + TALER_exchange_online_age_withdraw_confirmation_sign ( + &TEH_keys_exchange_sign_, + ach, + noreveal_index, + &pub, + &sig); + + if (TALER_EC_NONE != ec) + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("noreveal_index", + noreveal_index), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * Check if the request is replayed and we already have an + * answer. If so, replay the existing answer and return the + * HTTP response. + * + * @param con connection to the client + * @param[in,out] awc parsed request data + * @param[out] mret HTTP status, set if we return true + * @return true if the request is idempotent with an existing request + * false if we did not find the request in the DB and did not set @a mret + */ +static bool +request_is_idempotent (struct MHD_Connection *con, + struct AgeWithdrawContext *awc, + MHD_RESULT *mret) +{ + enum GNUNET_DB_QueryStatus qs; + struct TALER_EXCHANGEDB_AgeWithdraw commitment; + + qs = TEH_plugin->get_age_withdraw (TEH_plugin->cls, + &awc->commitment.reserve_pub, + &awc->commitment.h_commitment, + &commitment); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mret = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_age_withdraw"); + return true; /* Well, kind-of. At least we have set mret. */ + } + + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return false; + + /* Generate idempotent reply */ + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++; + *mret = reply_age_withdraw_success (con, + &commitment.h_commitment, + commitment.noreveal_index); + return true; +} + + +/** + * Function called to iterate over KYC-relevant transaction amounts for a + * particular time range. Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and account to iterate + * over events for + * @param limit maximum time-range for which events should be fetched + * (timestamp in the past) + * @param cb function to call on each event found, events must be returned + * in reverse chronological order + * @param cb_cls closure for @a cb, of type struct AgeWithdrawContext + */ +static void +age_withdraw_amount_cb (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct AgeWithdrawContext *awc = cls; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signaling amount %s for KYC check during age-withdrawal\n", + TALER_amount2s (&awc->commitment.amount_with_fee)); + if (GNUNET_OK != + cb (cb_cls, + &awc->commitment.amount_with_fee, + awc->now.abs_time)) + return; + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (TEH_plugin->cls, + &awc->h_payto, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this age-withdrawal and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); +} + + +/** + * Function implementing age withdraw transaction. Runs the + * transaction logic; IF it returns a non-error code, the transaction + * logic MUST NOT queue a MHD response. IF it returns an hard error, + * the transaction logic MUST queue a MHD response and set @a mhd_ret. + * IF it returns the soft error code, the function MAY be called again + * to retry and MUST not queue a MHD response. + * + * @param cls a `struct AgeWithdrawContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +age_withdraw_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AgeWithdrawContext *awc = cls; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls, + &awc->commitment.reserve_pub, + &awc->h_payto); + if (qs < 0) + return qs; + + /* If _no_ results, reserve was created by merge, + in which case no KYC check is required as the + merge already did that. */ + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + char *kyc_required; + + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW, + &awc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &age_withdraw_amount_cb, + awc, + &kyc_required); + + if (qs < 0) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "kyc_test_required"); + } + return qs; + } + + if (NULL != kyc_required) + { + /* Mark result and return by inserting KYC requirement into DB! */ + awc->kyc.ok = false; + return TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + kyc_required, + &awc->h_payto, + &awc->commitment.reserve_pub, + &awc->kyc.requirement_row); + } + } + + awc->kyc.ok = true; + + /* KYC requirement fulfilled, do the age-withdraw transaction */ + { + bool found = false; + bool balance_ok = false; + bool age_ok = false; + bool conflict = false; + uint16_t allowed_maximum_age = 0; + uint32_t reserve_birthday = 0; + struct TALER_Amount reserve_balance; + + qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls, + &awc->commitment, + awc->now, + &found, + &balance_ok, + &reserve_balance, + &age_ok, + &allowed_maximum_age, + &reserve_birthday, + &conflict); + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_age_withdraw"); + return qs; + } + if (! found) + { + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! age_ok) + { + enum TALER_ErrorCode ec = + TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE; + + *mhd_ret = + TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_MHD_PACK_EC (ec), + GNUNET_JSON_pack_uint64 ("allowed_maximum_age", + allowed_maximum_age), + GNUNET_JSON_pack_uint64 ("reserve_birthday", + reserve_birthday)); + + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + TEH_plugin->rollback (TEH_plugin->cls); + + *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( + connection, + TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS, + &reserve_balance, + &awc->commitment.amount_with_fee, + &awc->commitment.reserve_pub); + + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (conflict) + { + /* do_age_withdraw signaled a conflict, so there MUST be an entry + * in the DB. Put that into the response */ + bool ok = request_is_idempotent (connection, + awc, + mhd_ret); + GNUNET_assert (ok); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + *mhd_ret = -1; + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW]++; + return qs; +} + + +/** + * @brief Sign the chosen blinded coins, debit the reserve and persist + * the commitment. + * + * On conflict, the noreveal_index from the previous, existing + * commitment is returned to the client, returning success. + * + * On error (like, insufficient funds), the client is notified. + * + * Note that on success, there are two possible states: + * 1.) KYC is required (awc.kyc.ok == false) or + * 2.) age withdraw was successful. + * + * @param connection HTTP-connection to the client + * @param awc The context for the current age withdraw request + * @param[out] result On error, a HTTP-response will be queued and result set accordingly + * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise + */ +static enum GNUNET_GenericReturnValue +sign_and_do_age_withdraw ( + struct MHD_Connection *connection, + struct AgeWithdrawContext *awc, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct TALER_BlindedCoinHashP h_coin_evs[awc->num_coins]; + struct TALER_BlindedDenominationSignature denom_sigs[awc->num_coins]; + uint8_t noreveal_index; + + awc->now = GNUNET_TIME_timestamp_get (); + + /* Pick the challenge */ + noreveal_index = + GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, + TALER_CNC_KAPPA); + + awc->commitment.noreveal_index = noreveal_index; + + /* Choose and sign the coins */ + { + struct TEH_CoinSignData csds[awc->num_coins]; + enum TALER_ErrorCode ec; + + /* Pick the chosen blinded coins */ + for (uint32_t i = 0; i<awc->num_coins; i++) + { + csds[i].bp = &awc->coin_evs[i][noreveal_index]; + csds[i].h_denom_pub = &awc->denom_hs[i]; + } + + ec = TEH_keys_denomination_batch_sign (awc->num_coins, + csds, + false, + denom_sigs); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (connection, + ec, + NULL); + return GNUNET_SYSERR; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Signatures ready, starting DB interaction\n"); + + /* Prepare the hashes of the coins for insertion */ + for (uint32_t i = 0; i<awc->num_coins; i++) + { + TALER_coin_ev_hash (&awc->coin_evs[i][noreveal_index], + &awc->denom_hs[i], + &h_coin_evs[i]); + } + + /* Run the transaction */ + awc->commitment.h_coin_evs = h_coin_evs; + awc->commitment.denom_sigs = denom_sigs; + ret = TEH_DB_run_transaction (connection, + "run age withdraw", + TEH_MT_REQUEST_AGE_WITHDRAW, + result, + &age_withdraw_transaction, + awc); + /* Free resources */ + for (unsigned int i = 0; i<awc->num_coins; i++) + TALER_blinded_denom_sig_free (&denom_sigs[i]); + awc->commitment.h_coin_evs = NULL; + awc->commitment.denom_sigs = NULL; + return ret; +} + + +MHD_RESULT +TEH_handler_age_withdraw (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + MHD_RESULT mhd_ret; + const json_t *j_denom_hs; + const json_t *j_blinded_coin_evs; + struct AgeWithdrawContext awc = {0}; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("denom_hs", + &j_denom_hs), + GNUNET_JSON_spec_array_const ("blinded_coin_evs", + &j_blinded_coin_evs), + GNUNET_JSON_spec_uint16 ("max_age", + &awc.commitment.max_age), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &awc.commitment.reserve_sig), + GNUNET_JSON_spec_end () + }; + + awc.commitment.reserve_pub = *reserve_pub; + + + /* Parse the JSON body */ + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + + do { + /* Note: If we break the statement here at any point, + * a response to the client MUST have been populated + * with an appropriate answer and mhd_ret MUST have + * been set accordingly. + */ + + /* Parse denoms_h and blinded_coins_evs, partially fill awc */ + if (GNUNET_OK != + parse_age_withdraw_json (rc->connection, + j_denom_hs, + j_blinded_coin_evs, + &awc, + &mhd_ret)) + break; + + /* Ensure validity of denoms and calculate amounts and fees */ + if (GNUNET_OK != + are_denominations_valid (rc->connection, + awc.num_coins, + awc.denom_hs, + awc.coin_evs, + &awc.commitment.denom_serials, + &awc.commitment.amount_with_fee, + &mhd_ret)) + break; + + /* Now that amount_with_fee is calculated, verify the signature of + * the request body with the reserve key. + */ + if (GNUNET_OK != + verify_reserve_signature (rc->connection, + &awc.commitment, + &mhd_ret)) + break; + + /* Sign the chosen blinded coins, persist the commitment and + * charge the reserve. + * On error (like, insufficient funds), the client is notified. + * On conflict, the noreveal_index from the previous, existing + * commitment is returned to the client, returning success. + * Note that on success, there are two possible states: + * KYC is required (awc.kyc.ok == false) or + * age withdraw was successful. + */ + if (GNUNET_OK != + sign_and_do_age_withdraw (rc->connection, + &awc, + &mhd_ret)) + break; + + /* Send back final response, depending on the outcome of + * the DB-transaction */ + if (! awc.kyc.ok) + mhd_ret = TEH_RESPONSE_reply_kyc_required (rc->connection, + &awc.h_payto, + &awc.kyc); + else + mhd_ret = reply_age_withdraw_success (rc->connection, + &awc.commitment.h_commitment, + awc.commitment.noreveal_index); + + } while (0); + + GNUNET_JSON_parse_free (spec); + free_age_withdraw_context_resources (&awc); + return mhd_ret; + +} + + +/* end of taler-exchange-httpd_age-withdraw.c */ diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.h b/src/exchange/taler-exchange-httpd_age-withdraw.h new file mode 100644 index 000000000..a76779190 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_age-withdraw.h @@ -0,0 +1,47 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_age-withdraw.h + * @brief Handle /reserve/$RESERVE_PUB/age-withdraw requests + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H +#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/reserves/$RESERVE_PUB/age-withdraw" request. + * + * Parses the batch of commitments to withdraw age restricted coins, and checks + * that the signature "reserve_sig" makes this a valid withdrawal request from + * the specified reserve. If the request is valid, the response contains a + * noreveal_index which the client has to use for the subsequent call to + * /age-withdraw/$ACH/reveal. + * + * @param rc request context + * @param root uploaded JSON data + * @param reserve_pub public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_age_withdraw (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c new file mode 100644 index 000000000..c9aca8e99 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c @@ -0,0 +1,610 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_age-withdraw_reveal.c + * @brief Handle /age-withdraw/$ACH/reveal requests + * @author Özgür Kesim + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler-exchange-httpd_metrics.h" +#include "taler_error_codes.h" +#include "taler_exchangedb_plugin.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_age-withdraw_reveal.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" + +/** + * State for an /age-withdraw/$ACH/reveal operation. + */ +struct AgeRevealContext +{ + + /** + * Commitment for the age-withdraw operation, previously called by the + * client. + */ + struct TALER_AgeWithdrawCommitmentHashP ach; + + /** + * Public key of the reserve for with the age-withdraw commitment was + * originally made. This parameter is provided by the client again + * during the call to reveal in order to save a database-lookup. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Number of coins to reveal. MUST be equal to + * @e num_secrets/(kappa -1). + */ + uint32_t num_coins; + + /** + * Number of secrets in the reveal. MUST be a multiple of (kappa-1). + */ + uint32_t num_secrets; + + /** + * @e num_secrets secrets for disclosed coins. + */ + struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets; + + /** + * The data from the original age-withdraw. Will be retrieved from + * the DB via @a ach and @a reserve_pub. + */ + struct TALER_EXCHANGEDB_AgeWithdraw commitment; +}; + + +/** + * Parse the json body of an '/age-withdraw/$ACH/reveal' request. It extracts + * the denomination hashes, blinded coins and disclosed coins and allocates + * memory for those. + * + * @param connection The MHD connection to handle + * @param j_disclosed_coin_secrets The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from + * @param[out] actx The context of the operation, only partially built at call time + * @param[out] mhd_ret The result if a reply is queued for MHD + * @return true on success, false on failure, with a reply already queued for MHD. + */ +static enum GNUNET_GenericReturnValue +parse_age_withdraw_reveal_json ( + struct MHD_Connection *connection, + const json_t *j_disclosed_coin_secrets, + struct AgeRevealContext *actx, + MHD_RESULT *mhd_ret) +{ + enum GNUNET_GenericReturnValue result = GNUNET_SYSERR; + size_t num_entries; + + /* Verify JSON-structure consistency */ + { + const char *error = NULL; + + num_entries = json_array_size (j_disclosed_coin_secrets); /* 0, if not an array */ + + if (! json_is_array (j_disclosed_coin_secrets)) + error = "disclosed_coin_secrets must be an array"; + else if (num_entries == 0) + error = "disclosed_coin_secrets must not be empty"; + else if (num_entries > TALER_MAX_FRESH_COINS) + error = "maximum number of coins that can be withdrawn has been exceeded"; + + if (NULL != error) + { + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + error); + return GNUNET_SYSERR; + } + + actx->num_secrets = num_entries * (TALER_CNC_KAPPA - 1); + actx->num_coins = num_entries; + + } + + /* Continue parsing the parts */ + { + unsigned int idx = 0; + unsigned int k = 0; + json_t *array = NULL; + json_t *value = NULL; + + /* Parse diclosed keys */ + actx->disclosed_coin_secrets = + GNUNET_new_array (actx->num_secrets, + struct TALER_PlanchetMasterSecretP); + + json_array_foreach (j_disclosed_coin_secrets, idx, array) { + if (! json_is_array (array) || + (TALER_CNC_KAPPA - 1 != json_array_size (array))) + { + char msg[256] = {0}; + GNUNET_snprintf (msg, + sizeof(msg), + "couldn't parse entry no. %d in array disclosed_coin_secrets", + idx + 1); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + msg); + goto EXIT; + + } + + json_array_foreach (array, k, value) + { + struct TALER_PlanchetMasterSecretP *secret = + &actx->disclosed_coin_secrets[2 * idx + k]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, secret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, spec, NULL, NULL)) + { + char msg[256] = {0}; + GNUNET_snprintf (msg, + sizeof(msg), + "couldn't parse entry no. %d in array disclosed_coin_secrets[%d]", + k + 1, + idx + 1); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + msg); + goto EXIT; + } + } + }; + } + + result = GNUNET_OK; + +EXIT: + return result; +} + + +/** + * Check if the request belongs to an existing age-withdraw request. + * If so, sets the commitment object with the request data. + * Otherwise, it queues an appropriate MHD response. + * + * @param connection The HTTP connection to the client + * @param h_commitment Original commitment value sent with the age-withdraw request + * @param reserve_pub Reserve public key used in the original age-withdraw request + * @param[out] commitment Data from the original age-withdraw request + * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. + * @return #GNUNET_OK if the withdraw request has been found, + * #GNUNET_SYSERR if we did not find the request in the DB + */ +static enum GNUNET_GenericReturnValue +find_original_commitment ( + struct MHD_Connection *connection, + const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, + const struct TALER_ReservePublicKeyP *reserve_pub, + struct TALER_EXCHANGEDB_AgeWithdraw *commitment, + MHD_RESULT *result) +{ + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int try = 0; try < 3; try++) + { + qs = TEH_plugin->get_age_withdraw (TEH_plugin->cls, + reserve_pub, + h_commitment, + commitment); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return GNUNET_OK; /* Only happy case */ + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *result = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_HARD_ERROR: + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_age_withdraw_info"); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + break; /* try again */ + default: + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + return GNUNET_SYSERR; + } + } + /* after unsuccessful retries*/ + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_age_withdraw_info"); + return GNUNET_SYSERR; +} + + +/** + * @brief Derives a age-restricted planchet from a given secret and calculates the hash + * + * @param connection Connection to the client + * @param keys The denomination keys in memory + * @param secret The secret to a planchet + * @param denom_pub_h The hash of the denomination for the planchet + * @param max_age The maximum age allowed + * @param[out] bch Hashcode to write + * @param[out] result On error, a HTTP-response will be queued and result set accordingly + * @return GNUNET_OK on success, GNUNET_SYSERR otherwise, with an error message + * written to the client and @e result set. + */ +static enum GNUNET_GenericReturnValue +calculate_blinded_hash ( + struct MHD_Connection *connection, + const struct TEH_KeyStateHandle *keys, + const struct TALER_PlanchetMasterSecretP *secret, + const struct TALER_DenominationHashP *denom_pub_h, + uint8_t max_age, + struct TALER_BlindedCoinHashP *bch, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret; + struct TEH_DenominationKey *denom_key; + struct TALER_AgeCommitmentHash ach; + + /* First, retrieve denomination details */ + denom_key = TEH_keys_denomination_by_hash_from_state (keys, + denom_pub_h, + connection, + result); + if (NULL == denom_key) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + /* calculate age commitment hash */ + { + struct TALER_AgeCommitmentProof acp; + + TALER_age_restriction_from_secret (secret, + &denom_key->denom_pub.age_mask, + max_age, + &acp); + TALER_age_commitment_hash (&acp.commitment, + &ach); + TALER_age_commitment_proof_free (&acp); + } + + /* Next: calculate planchet */ + { + struct TALER_CoinPubHashP c_hash; + struct TALER_PlanchetDetail detail = {0}; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; + struct GNUNET_CRYPTO_BlindingInputValues bi = { + .cipher = denom_key->denom_pub.bsign_pub_key->cipher + }; + struct TALER_ExchangeWithdrawValues alg_values = { + .blinding_inputs = &bi + }; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + union GNUNET_CRYPTO_BlindSessionNonce *noncep = NULL; + + // FIXME: add logic to denom.c to do this! + if (GNUNET_CRYPTO_BSA_CS == bi.cipher) + { + struct TEH_CsDeriveData cdd = { + .h_denom_pub = &denom_key->h_denom_pub, + .nonce = &nonce.cs_nonce, + }; + + TALER_cs_withdraw_nonce_derive (secret, + &nonce.cs_nonce); + noncep = &nonce; + GNUNET_assert (TALER_EC_NONE == + TEH_keys_denomination_cs_r_pub ( + &cdd, + false, + &bi.details.cs_values)); + } + TALER_planchet_blinding_secret_create (secret, + &alg_values, + &bks); + TALER_planchet_setup_coin_priv (secret, + &alg_values, + &coin_priv); + ret = TALER_planchet_prepare (&denom_key->denom_pub, + &alg_values, + &bks, + noncep, + &coin_priv, + &ach, + &c_hash, + &detail); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + *result = TALER_MHD_reply_json_pack (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "{ss}", + "details", + "failed to prepare planchet from base key"); + return ret; + } + + TALER_coin_ev_hash (&detail.blinded_planchet, + &denom_key->h_denom_pub, + bch); + TALER_blinded_planchet_free (&detail.blinded_planchet); + } + + return ret; +} + + +/** + * @brief Checks the validity of the disclosed coins as follows: + * - Derives and calculates the disclosed coins' + * - public keys, + * - nonces (if applicable), + * - age commitments, + * - blindings + * - blinded hashes + * - Computes h_commitment with those calculated and the undisclosed hashes + * - Compares h_commitment with the value from the original commitment + * - Verifies that all public keys in indices larger than the age group + * corresponding to max_age are derived from the constant public key. + * + * The derivation of the blindings, (potential) nonces and age-commitment from + * a coin's private keys is defined in + * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw + * + * @param connection HTTP-connection to the client + * @param commitment Original commitment + * @param disclosed_coin_secrets The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many + * @param num_coins number of coins to reveal via @a disclosed_coin_secrets + * @param[out] result On error, a HTTP-response will be queued and result set accordingly + * @return GNUNET_OK on success, GNUNET_SYSERR otherwise + */ +static enum GNUNET_GenericReturnValue +verify_commitment_and_max_age ( + struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, + const struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets, + uint32_t num_coins, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct GNUNET_HashContext *hash_context; + struct TEH_KeyStateHandle *keys; + + if (num_coins != commitment->num_coins) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "#coins"); + return GNUNET_SYSERR; + } + + /* We need the current keys in memory for the meta-data of the denominations */ + keys = TEH_keys_get_state (); + if (NULL == keys) + { + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + hash_context = GNUNET_CRYPTO_hash_context_start (); + + for (size_t coin_idx = 0; coin_idx < num_coins; coin_idx++) + { + size_t i = 0; /* either 0 or 1, to index into coin_evs */ + + for (size_t k = 0; k<TALER_CNC_KAPPA; k++) + { + if (k == (size_t) commitment->noreveal_index) + { + GNUNET_CRYPTO_hash_context_read (hash_context, + &commitment->h_coin_evs[coin_idx], + sizeof(commitment->h_coin_evs[coin_idx])); + } + else + { + /* j is the index into disclosed_coin_secrets[] */ + size_t j = (TALER_CNC_KAPPA - 1) * coin_idx + i; + const struct TALER_PlanchetMasterSecretP *secret; + struct TALER_BlindedCoinHashP bch; + + GNUNET_assert (2>i); + GNUNET_assert ((TALER_CNC_KAPPA - 1) * num_coins > j); + + secret = &disclosed_coin_secrets[j]; + i++; + + ret = calculate_blinded_hash (connection, + keys, + secret, + &commitment->denom_pub_hashes[coin_idx], + commitment->max_age, + &bch, + result); + + if (GNUNET_OK != ret) + { + GNUNET_CRYPTO_hash_context_abort (hash_context); + return GNUNET_SYSERR; + } + + /* Continue the running hash of all coin hashes with the calculated + * hash-value of the current, disclosed coin */ + GNUNET_CRYPTO_hash_context_read (hash_context, + &bch, + sizeof(bch)); + } + } + } + + /* Finally, compare the calculated hash with the original commitment */ + { + struct GNUNET_HashCode calc_hash; + GNUNET_CRYPTO_hash_context_finish (hash_context, + &calc_hash); + + if (0 != GNUNET_CRYPTO_hash_cmp (&commitment->h_commitment.hash, + &calc_hash)) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH, + NULL); + return GNUNET_SYSERR; + } + + } + return GNUNET_OK; +} + + +/** + * @brief Send a response for "/age-withdraw/$RCH/reveal" + * + * @param connection The http connection to the client to send the response to + * @param commitment The data from the commitment with signatures + * @return a MHD result code + */ +static MHD_RESULT +reply_age_withdraw_reveal_success ( + struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_AgeWithdraw *commitment) +{ + json_t *list = json_array (); + GNUNET_assert (NULL != list); + + for (unsigned int i = 0; i < commitment->num_coins; i++) + { + json_t *obj = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig (NULL, + &commitment->denom_sigs[i])); + GNUNET_assert (0 == + json_array_append_new (list, + obj)); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + list)); +} + + +MHD_RESULT +TEH_handler_age_withdraw_reveal ( + struct TEH_RequestContext *rc, + const struct TALER_AgeWithdrawCommitmentHashP *ach, + const json_t *root) +{ + MHD_RESULT result = MHD_NO; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct AgeRevealContext actx = {0}; + const json_t *j_disclosed_coin_secrets; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &actx.reserve_pub), + GNUNET_JSON_spec_array_const ("disclosed_coin_secrets", + &j_disclosed_coin_secrets), + GNUNET_JSON_spec_end () + }; + + actx.ach = *ach; + + /* Parse JSON body*/ + ret = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; + } + + + do { + /* Extract denominations, blinded and disclosed coins */ + if (GNUNET_OK != + parse_age_withdraw_reveal_json ( + rc->connection, + j_disclosed_coin_secrets, + &actx, + &result)) + break; + + /* Find original commitment */ + if (GNUNET_OK != + find_original_commitment ( + rc->connection, + &actx.ach, + &actx.reserve_pub, + &actx.commitment, + &result)) + break; + + /* Verify the computed h_commitment equals the committed one and that coins + * have a maximum age group corresponding max_age (age-mask dependent) */ + if (GNUNET_OK != + verify_commitment_and_max_age ( + rc->connection, + &actx.commitment, + actx.disclosed_coin_secrets, + actx.num_coins, + &result)) + break; + + /* Finally, return the signatures */ + result = reply_age_withdraw_reveal_success (rc->connection, + &actx.commitment); + + } while (0); + + GNUNET_JSON_parse_free (spec); + if (NULL != actx.commitment.denom_sigs) + for (unsigned int i = 0; i<actx.num_coins; i++) + TALER_blinded_denom_sig_free (&actx.commitment.denom_sigs[i]); + GNUNET_free (actx.commitment.denom_sigs); + GNUNET_free (actx.commitment.denom_pub_hashes); + GNUNET_free (actx.commitment.denom_serials); + GNUNET_free (actx.disclosed_coin_secrets); + return result; +} + + +/* end of taler-exchange-httpd_age-withdraw_reveal.c */ diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h new file mode 100644 index 000000000..f7b813fe7 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h @@ -0,0 +1,56 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_age-withdraw_reveal.h + * @brief Handle /age-withdraw/$ACH/reveal requests + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H +#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/age-withdraw/$ACH/reveal" request. + * + * The client got a noreveal_index in response to a previous request + * /reserve/$RESERVE_PUB/age-withdraw. It now has to reveal all n*(kappa-1) + * coin's private keys (except for the noreveal_index), from which all other + * coin-relevant data (blinding, age restriction, nonce) is derived from. + * + * The exchange computes those values, ensures that the maximum age is + * correctly applied, calculates the hash of the blinded envelopes, and - + * together with the non-disclosed blinded envelopes - compares the hash of + * those against the original commitment $ACH. + * + * If all those checks and the used denominations turn out to be correct, the + * exchange signs all blinded envelopes with their appropriate denomination + * keys. + * + * @param rc request context + * @param root uploaded JSON data + * @param ach commitment to the age restricted coints from the age-withdraw request + * @return MHD result code + */ +MHD_RESULT +TEH_handler_age_withdraw_reveal ( + struct TEH_RequestContext *rc, + const struct TALER_AgeWithdrawCommitmentHashP *ach, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_aml-decision-get.c b/src/exchange/taler-exchange-httpd_aml-decision-get.c new file mode 100644 index 000000000..b4f337db1 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_aml-decision-get.c @@ -0,0 +1,233 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_aml-decision-get.c + * @brief Return summary information about AML decision + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_aml-decision.h" +#include "taler-exchange-httpd_metrics.h" + + +/** + * Maximum number of records we return per request. + */ +#define MAX_RECORDS 1024 + +/** + * Callback with KYC attributes about a particular user. + * + * @param[in,out] cls closure with a `json_t *` array to update + * @param h_payto account for which the attribute data is stored + * @param provider_section provider that must be checked + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + */ +static void +kyc_attribute_cb ( + void *cls, + const struct TALER_PaytoHashP *h_payto, + const char *provider_section, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes) +{ + json_t *kyc_attributes = cls; + json_t *attributes; + + attributes = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key, + enc_attributes, + enc_attributes_size); + GNUNET_break (NULL != attributes); + GNUNET_assert ( + 0 == + json_array_append ( + kyc_attributes, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("provider_section", + provider_section), + GNUNET_JSON_pack_timestamp ("collection_time", + collection_time), + GNUNET_JSON_pack_timestamp ("expiration_time", + expiration_time), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("attributes", + attributes)) + ))); +} + + +/** + * Return historic AML decision(s). + * + * @param[in,out] cls closure with a `json_t *` array to update + * @param new_threshold new monthly threshold that would trigger an AML check + * @param new_state AML decision status + * @param decision_time when was the decision made + * @param justification human-readable text justifying the decision + * @param decider_pub public key of the staff member + * @param decider_sig signature of the staff member + */ +static void +aml_history_cb ( + void *cls, + const struct TALER_Amount *new_threshold, + enum TALER_AmlDecisionState new_state, + struct GNUNET_TIME_Timestamp decision_time, + const char *justification, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const struct TALER_AmlOfficerSignatureP *decider_sig) +{ + json_t *aml_history = cls; + + GNUNET_assert ( + 0 == + json_array_append ( + aml_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("decider_pub", + decider_pub), + GNUNET_JSON_pack_string ("justification", + justification), + TALER_JSON_pack_amount ("new_threshold", + new_threshold), + GNUNET_JSON_pack_int64 ("new_state", + new_state), + GNUNET_JSON_pack_timestamp ("decision_time", + decision_time) + ))); +} + + +MHD_RESULT +TEH_handler_aml_decision_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + struct TALER_PaytoHashP h_payto; + + if ( (NULL == args[0]) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &h_payto, + sizeof (h_payto))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "h_payto"); + } + + if (NULL != args[1]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[1]); + } + + { + json_t *aml_history; + json_t *kyc_attributes; + enum GNUNET_DB_QueryStatus qs; + bool none = false; + + aml_history = json_array (); + GNUNET_assert (NULL != aml_history); + qs = TEH_plugin->select_aml_history (TEH_plugin->cls, + &h_payto, + &aml_history_cb, + aml_history); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + json_decref (aml_history); + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + none = true; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + none = false; + break; + } + + kyc_attributes = json_array (); + GNUNET_assert (NULL != kyc_attributes); + qs = TEH_plugin->select_kyc_attributes (TEH_plugin->cls, + &h_payto, + &kyc_attribute_cb, + kyc_attributes); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + json_decref (aml_history); + json_decref (kyc_attributes); + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (none) + { + json_decref (aml_history); + json_decref (kyc_attributes); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("aml_history", + aml_history), + GNUNET_JSON_pack_array_steal ("kyc_attributes", + kyc_attributes)); + } +} + + +/* end of taler-exchange-httpd_aml-decision_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-decision.c b/src/exchange/taler-exchange-httpd_aml-decision.c new file mode 100644 index 000000000..bf43fdbf2 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_aml-decision.c @@ -0,0 +1,358 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_aml-decision.c + * @brief Handle request about an AML decision. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for #make_aml_decision() + */ +struct DecisionContext +{ + /** + * Justification given for the decision. + */ + const char *justification; + + /** + * When was the decision taken. + */ + struct GNUNET_TIME_Timestamp decision_time; + + /** + * New threshold for revising the decision. + */ + struct TALER_Amount new_threshold; + + /** + * Hash of payto://-URI of affected account. + */ + struct TALER_PaytoHashP h_payto; + + /** + * New AML state. + */ + enum TALER_AmlDecisionState new_state; + + /** + * Signature affirming the decision. + */ + struct TALER_AmlOfficerSignatureP officer_sig; + + /** + * Public key of the AML officer. + */ + const struct TALER_AmlOfficerPublicKeyP *officer_pub; + + /** + * KYC requirements imposed, NULL for none. + */ + const json_t *kyc_requirements; + +}; + + +/** + * Function implementing AML decision database transaction. + * + * Runs the transaction logic; IF it returns a non-error code, the + * transaction logic MUST NOT queue a MHD response. IF it returns an hard + * error, the transaction logic MUST queue a MHD response and set @a mhd_ret. + * IF it returns the soft error code, the function MAY be called again to + * retry and MUST not queue a MHD response. + * + * @param cls closure with a `struct DecisionContext` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +make_aml_decision (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct DecisionContext *dc = cls; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp last_date; + bool invalid_officer; + uint64_t requirement_row = 0; + + if ( (NULL != dc->kyc_requirements) && + (0 != json_array_size (dc->kyc_requirements)) ) + { + char *res = NULL; + size_t idx; + json_t *req; + bool satisfied; + + json_array_foreach (dc->kyc_requirements, idx, req) + { + const char *r = json_string_value (req); + + if (NULL == res) + { + res = GNUNET_strdup (r); + } + else + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s %s", + res, + r); + GNUNET_free (res); + res = tmp; + } + } + + { + json_t *kyc_details = NULL; + + qs = TALER_KYCLOGIC_check_satisfied ( + &res, + &dc->h_payto, + &kyc_details, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &satisfied); + json_decref (kyc_details); + } + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_satisfied_kyc_processes"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; + } + if (! satisfied) + { + qs = TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + res, + &dc->h_payto, + NULL, /* not a reserve */ + &requirement_row); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_for_account"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; + } + } + GNUNET_free (res); + } + + qs = TEH_plugin->insert_aml_decision (TEH_plugin->cls, + &dc->h_payto, + &dc->new_threshold, + dc->new_state, + dc->decision_time, + dc->justification, + dc->kyc_requirements, + requirement_row, + dc->officer_pub, + &dc->officer_sig, + &invalid_officer, + &last_date); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_aml_decision"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; + } + if (invalid_officer) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_AML_DECISION_INVALID_OFFICER, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (GNUNET_TIME_timestamp_cmp (last_date, + >=, + dc->decision_time)) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +MHD_RESULT +TEH_handler_post_aml_decision ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const json_t *root) +{ + struct MHD_Connection *connection = rc->connection; + struct DecisionContext dc = { + .officer_pub = officer_pub + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("officer_sig", + &dc.officer_sig), + GNUNET_JSON_spec_fixed_auto ("h_payto", + &dc.h_payto), + TALER_JSON_spec_amount ("new_threshold", + TEH_currency, + &dc.new_threshold), + GNUNET_JSON_spec_string ("justification", + &dc.justification), + GNUNET_JSON_spec_timestamp ("decision_time", + &dc.decision_time), + TALER_JSON_spec_aml_decision ("new_state", + &dc.new_state), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("kyc_requirements", + &dc.kyc_requirements), + NULL), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_officer_aml_decision_verify (dc.justification, + dc.decision_time, + &dc.new_threshold, + &dc.h_payto, + dc.new_state, + dc.kyc_requirements, + dc.officer_pub, + &dc.officer_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID, + NULL); + } + + if (NULL != dc.kyc_requirements) + { + size_t index; + json_t *elem; + + json_array_foreach (dc.kyc_requirements, index, elem) + { + const char *val; + + if (! json_is_string (elem)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kyc_requirements array members must be strings"); + } + val = json_string_value (elem); + if (GNUNET_SYSERR == + TALER_KYCLOGIC_check_satisfiable (val)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_AML_DECISION_UNKNOWN_CHECK, + val); + } + } + } + + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "make-aml-decision", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &make_aml_decision, + &dc)) + { + return mhd_ret; + } + } + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_aml-decision.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-decision.h b/src/exchange/taler-exchange-httpd_aml-decision.h new file mode 100644 index 000000000..8af742c0a --- /dev/null +++ b/src/exchange/taler-exchange-httpd_aml-decision.h @@ -0,0 +1,79 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_aml-decision.h + * @brief Handle /aml/$OFFICER_PUB/decision requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_AML_DECISION_H +#define TALER_EXCHANGE_HTTPD_AML_DECISION_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a POST "/aml/$OFFICER_PUB/decision" request. Parses the decision + * details, checks the signatures and if appropriately authorized executes + * the decision. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_post_aml_decision ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const json_t *root); + + +/** + * Handle a GET "/aml/$OFFICER_PUB/decisions/$STATE" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching decisions. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be the state) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_decisions_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +/** + * Handle a GET "/aml/$OFFICER_PUB/decision/$H_PAYTO" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the AML history and KYC attributes for the account. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be one) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_decision_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_aml-decisions-get.c b/src/exchange/taler-exchange-httpd_aml-decisions-get.c new file mode 100644 index 000000000..763817cf6 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_aml-decisions-get.c @@ -0,0 +1,215 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_aml-decisions-get.c + * @brief Return summary information about AML decisions + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_aml-decision.h" +#include "taler-exchange-httpd_metrics.h" + + +/** + * Maximum number of records we return per request. + */ +#define MAX_RECORDS 1024 + +/** + * Return AML status. + * + * @param cls closure + * @param row_id current row in AML status table + * @param h_payto account for which the attribute data is stored + * @param threshold currently monthly threshold that would trigger an AML check + * @param status what is the current AML decision + */ +static void +record_cb ( + void *cls, + uint64_t row_id, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_Amount *threshold, + enum TALER_AmlDecisionState status) +{ + json_t *records = cls; + + GNUNET_assert ( + 0 == + json_array_append ( + records, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + GNUNET_JSON_pack_int64 ("current_state", + status), + TALER_JSON_pack_amount ("threshold", + threshold), + GNUNET_JSON_pack_int64 ("rowid", + row_id) + ))); +} + + +MHD_RESULT +TEH_handler_aml_decisions_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + enum TALER_AmlDecisionState decision; + int delta = -20; + unsigned long long start; + const char *state_str = args[0]; + + if (NULL == state_str) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[0]); + } + if (0 == strcmp (state_str, + "pending")) + decision = TALER_AML_PENDING; + else if (0 == strcmp (state_str, + "frozen")) + decision = TALER_AML_FROZEN; + else if (0 == strcmp (state_str, + "normal")) + decision = TALER_AML_NORMAL; + else + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + state_str); + } + if (NULL != args[1]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[1]); + } + + { + const char *p; + + p = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "delta"); + if (NULL != p) + { + char dummy; + + if (1 != sscanf (p, + "%d%c", + &delta, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "delta"); + } + } + if (delta > 0) + start = 0; + else + start = INT64_MAX; + p = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "start"); + if (NULL != p) + { + char dummy; + + if (1 != sscanf (p, + "%llu%c", + &start, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "start"); + } + } + } + + { + json_t *records; + enum GNUNET_DB_QueryStatus qs; + + records = json_array (); + GNUNET_assert (NULL != records); + if (INT_MIN == delta) + delta = INT_MIN + 1; + qs = TEH_plugin->select_aml_process (TEH_plugin->cls, + decision, + start, + GNUNET_MIN (MAX_RECORDS, + delta > 0 + ? delta + : -delta), + delta > 0, + &record_cb, + records); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + json_decref (records); + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("records", + records)); + } +} + + +/* end of taler-exchange-httpd_aml-decisions_get.c */ diff --git a/src/exchange/taler-exchange-httpd_auditors.c b/src/exchange/taler-exchange-httpd_auditors.c index 1b8af311c..9d3239e86 100644 --- a/src/exchange/taler-exchange-httpd_auditors.c +++ b/src/exchange/taler-exchange-httpd_auditors.c @@ -45,7 +45,7 @@ struct AddAuditorDenomContext /** * Denomination this is about. */ - const struct TALER_DenominationHash *h_denom_pub; + const struct TALER_DenominationHashP *h_denom_pub; /** * Auditor this is about. @@ -140,6 +140,7 @@ add_auditor_denom_sig (void *cls, TALER_B2S (awc->auditor_pub)); return GNUNET_DB_STATUS_HARD_ERROR; } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_auditor_denom_validity_verify ( auditor_url, @@ -150,10 +151,7 @@ add_auditor_denom_sig (void *cls, meta.expire_deposit, meta.expire_legal, &meta.value, - &meta.fee_withdraw, - &meta.fee_deposit, - &meta.fee_refresh, - &meta.fee_refund, + &meta.fees, awc->auditor_pub, &awc->auditor_sig)) { @@ -192,7 +190,7 @@ MHD_RESULT TEH_handler_auditors ( struct MHD_Connection *connection, const struct TALER_AuditorPublicKeyP *auditor_pub, - const struct TALER_DenominationHash *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const json_t *root) { struct AddAuditorDenomContext awc = { @@ -216,7 +214,7 @@ TEH_handler_auditors ( return MHD_YES; /* failure */ ret = TEH_DB_run_transaction (connection, "add auditor denom sig", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &res, &add_auditor_denom_sig, &awc); diff --git a/src/exchange/taler-exchange-httpd_auditors.h b/src/exchange/taler-exchange-httpd_auditors.h index 00a2e57a9..5d5c3a49c 100644 --- a/src/exchange/taler-exchange-httpd_auditors.h +++ b/src/exchange/taler-exchange-httpd_auditors.h @@ -39,7 +39,7 @@ MHD_RESULT TEH_handler_auditors ( struct MHD_Connection *connection, const struct TALER_AuditorPublicKeyP *auditor_pub, - const struct TALER_DenominationHash *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const json_t *root); diff --git a/src/exchange/taler-exchange-httpd_batch-deposit.c b/src/exchange/taler-exchange-httpd_batch-deposit.c new file mode 100644 index 000000000..84f27dd94 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_batch-deposit.c @@ -0,0 +1,738 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_batch-deposit.c + * @brief Handle /batch-deposit requests; parses the POST and JSON and + * verifies the coin signatures before handing things off + * to the database. + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_extensions_policy.h" +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_batch-deposit.h" +#include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_lib.h" +#include "taler-exchange-httpd_keys.h" + + +/** + * Closure for #batch_deposit_transaction. + */ +struct BatchDepositContext +{ + + /** + * Array with the individual coin deposit fees. + */ + struct TALER_Amount *deposit_fees; + + /** + * Our timestamp (when we received the request). + * Possibly updated by the transaction if the + * request is idempotent (was repeated). + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Details about the batch deposit operation. + */ + struct TALER_EXCHANGEDB_BatchDeposit bd; + + + /** + * Total amount that is accumulated with this deposit, + * without fee. + */ + struct TALER_Amount accumulated_total_without_fee; + + /** + * True, if no policy was present in the request. Then + * @e policy_json is NULL and @e h_policy will be all zero. + */ + bool has_no_policy; + + /** + * Additional details for policy extension relevant for this + * deposit operation, possibly NULL! + */ + json_t *policy_json; + + /** + * If @e policy_json was present, the corresponding policy extension + * calculates these details. These will be persisted in the policy_details + * table. + */ + struct TALER_PolicyDetails policy_details; + + /** + * Hash over @e policy_details, might be all zero + */ + struct TALER_ExtensionPolicyHashP h_policy; + + /** + * Hash over the merchant's payto://-URI with the wire salt. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * When @e policy_details are persisted, this contains the id of the record + * in the policy_details table. + */ + uint64_t policy_details_serial_id; + +}; + + +/** + * Send confirmation of batch deposit success to client. This function will + * create a signed message affirming the given information and return it to + * the client. By this, the exchange affirms that the coins had sufficient + * (residual) value for the specified transaction and that it will execute the + * requested batch deposit operation with the given wiring details. + * + * @param connection connection to the client + * @param dc information about the batch deposit + * @return MHD result code + */ +static MHD_RESULT +reply_batch_deposit_success ( + struct MHD_Connection *connection, + const struct BatchDepositContext *dc) +{ + const struct TALER_EXCHANGEDB_BatchDeposit *bd = &dc->bd; + const struct TALER_CoinSpendSignatureP *csigs[GNUNET_NZL (bd->num_cdis)]; + enum TALER_ErrorCode ec; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + for (unsigned int i = 0; i<bd->num_cdis; i++) + csigs[i] = &bd->cdis[i].csig; + if (TALER_EC_NONE != + (ec = TALER_exchange_online_deposit_confirmation_sign ( + &TEH_keys_exchange_sign_, + &bd->h_contract_terms, + &dc->h_wire, + dc->has_no_policy ? NULL : &dc->h_policy, + dc->exchange_timestamp, + bd->wire_deadline, + bd->refund_deadline, + &dc->accumulated_total_without_fee, + bd->num_cdis, + csigs, + &dc->bd.merchant_pub, + &pub, + &sig))) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + dc->exchange_timestamp), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig)); +} + + +/** + * Execute database transaction for /batch-deposit. Runs the transaction + * logic; IF it returns a non-error code, the transaction logic MUST + * NOT queue a MHD response. IF it returns an hard error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. IF + * it returns the soft error code, the function MAY be called again to + * retry and MUST not queue a MHD response. + * + * @param cls a `struct BatchDepositContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +batch_deposit_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct BatchDepositContext *dc = cls; + const struct TALER_EXCHANGEDB_BatchDeposit *bd = &dc->bd; + enum GNUNET_DB_QueryStatus qs = GNUNET_DB_STATUS_HARD_ERROR; + uint32_t bad_balance_coin_index = UINT32_MAX; + bool balance_ok; + bool in_conflict; + + /* If the deposit has a policy associated to it, persist it. This will + * insert or update the record. */ + if (! dc->has_no_policy) + { + qs = TEH_plugin->persist_policy_details ( + TEH_plugin->cls, + &dc->policy_details, + &dc->bd.policy_details_serial_id, + &dc->accumulated_total_without_fee, + &dc->policy_details.fulfillment_state); + if (qs < 0) + return qs; + + dc->bd.policy_blocked = + dc->policy_details.fulfillment_state != TALER_PolicyFulfillmentSuccess; + } + + /* FIXME: replace by batch insert! */ + for (unsigned int i = 0; i<bd->num_cdis; i++) + { + const struct TALER_EXCHANGEDB_CoinDepositInformation *cdi + = &bd->cdis[i]; + uint64_t known_coin_id; + + qs = TEH_make_coin_known (&cdi->coin, + connection, + &known_coin_id, + mhd_ret); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "make coin known (%s) returned %d\n", + TALER_B2S (&cdi->coin.coin_pub), + qs); + if (qs < 0) + return qs; + } + + qs = TEH_plugin->do_deposit ( + TEH_plugin->cls, + bd, + &dc->exchange_timestamp, + &balance_ok, + &bad_balance_coin_index, + &in_conflict); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ( + "Failed to store /batch-deposit information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "batch-deposit"); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "do_deposit returned: %d / %s[%u] / %s\n", + qs, + balance_ok ? "balance ok" : "balance insufficient", + (unsigned int) bad_balance_coin_index, + in_conflict ? "in conflict" : "no conflict"); + if (in_conflict) + { + struct TALER_MerchantWireHashP h_wire; + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TEH_plugin->get_wire_hash_for_contract ( + TEH_plugin->cls, + &bd->merchant_pub, + &bd->h_contract_terms, + &h_wire)) + { + TALER_LOG_WARNING ( + "Failed to retrieve conflicting contract details from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "batch-deposit"); + return qs; + } + + *mhd_ret + = TEH_RESPONSE_reply_coin_conflicting_contract ( + connection, + TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT, + &h_wire); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + GNUNET_assert (bad_balance_coin_index < bd->num_cdis); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "returning history of conflicting coin (%s)\n", + TALER_B2S (&bd->cdis[bad_balance_coin_index].coin.coin_pub)); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &bd->cdis[bad_balance_coin_index].coin.denom_pub_hash, + &bd->cdis[bad_balance_coin_index].coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT]++; + return qs; +} + + +/** + * Parse per-coin deposit information from @a jcoin + * into @a deposit. Fill in generic information from + * @a ctx. + * + * @param connection connection we are handling + * @param dc information about the overall batch + * @param jcoin coin data to parse + * @param[out] cdi where to store the result + * @param[out] deposit_fee where to write the deposit fee + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +static enum GNUNET_GenericReturnValue +parse_coin (struct MHD_Connection *connection, + const struct BatchDepositContext *dc, + json_t *jcoin, + struct TALER_EXCHANGEDB_CoinDepositInformation *cdi, + struct TALER_Amount *deposit_fee) +{ + const struct TALER_EXCHANGEDB_BatchDeposit *bd = &dc->bd; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("contribution", + TEH_currency, + &cdi->amount_with_fee), + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &cdi->coin.denom_pub_hash), + TALER_JSON_spec_denom_sig ("ub_sig", + &cdi->coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &cdi->coin.coin_pub), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &cdi->coin.h_age_commitment), + &cdi->coin.no_age_commitment), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &cdi->csig), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + if (GNUNET_OK != + (res = TALER_MHD_parse_json_data (connection, + jcoin, + spec))) + return res; + /* check denomination exists and is valid */ + { + struct TEH_DenominationKey *dk; + MHD_RESULT mret; + + dk = TEH_keys_denomination_by_hash (&cdi->coin.denom_pub_hash, + connection, + &mret); + if (NULL == dk) + { + GNUNET_JSON_parse_free (spec); + return (MHD_YES == mret) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (0 > TALER_amount_cmp (&dk->meta.value, + &cdi->amount_with_fee)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + { + /* This denomination is past the expiration time for deposits */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &cdi->coin.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "DEPOSIT")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &cdi->coin.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "DEPOSIT")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &cdi->coin.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "DEPOSIT")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (dk->denom_pub.bsign_pub_key->cipher != + cdi->coin.denom_sig.unblinded_sig->cipher) + { + /* denomination cipher and denomination signature cipher not the same */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + *deposit_fee = dk->meta.fees.deposit; + /* check coin signature */ + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } + if (GNUNET_YES != + TALER_test_coin_valid (&cdi->coin, + &dk->denom_pub)) + { + TALER_LOG_WARNING ("Invalid coin passed for /batch-deposit\n"); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + } + if (0 < TALER_amount_cmp (deposit_fee, + &cdi->amount_with_fee)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_deposit_verify ( + &cdi->amount_with_fee, + deposit_fee, + &dc->h_wire, + &bd->h_contract_terms, + &bd->wallet_data_hash, + cdi->coin.no_age_commitment + ? NULL + : &cdi->coin.h_age_commitment, + NULL != dc->policy_json ? &dc->h_policy : NULL, + &cdi->coin.denom_pub_hash, + bd->wallet_timestamp, + &bd->merchant_pub, + bd->refund_deadline, + &cdi->coin.coin_pub, + &cdi->csig)) + { + TALER_LOG_WARNING ("Invalid signature on /batch-deposit request\n"); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID, + TALER_B2S (&cdi->coin.coin_pub))) + ? GNUNET_NO + : GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_batch_deposit (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + struct MHD_Connection *connection = rc->connection; + struct BatchDepositContext dc = { 0 }; + struct TALER_EXCHANGEDB_BatchDeposit *bd = &dc.bd; + const json_t *coins; + bool no_refund_deadline = true; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_payto_uri ("merchant_payto_uri", + &bd->receiver_wire_account), + GNUNET_JSON_spec_fixed_auto ("wire_salt", + &bd->wire_salt), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &bd->merchant_pub), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &bd->h_contract_terms), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("wallet_data_hash", + &bd->wallet_data_hash), + &bd->no_wallet_data_hash), + GNUNET_JSON_spec_array_const ("coins", + &coins), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("policy", + &dc.policy_json), + &dc.has_no_policy), + GNUNET_JSON_spec_timestamp ("timestamp", + &bd->wallet_timestamp), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("refund_deadline", + &bd->refund_deadline), + &no_refund_deadline), + GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", + &bd->wire_deadline), + GNUNET_JSON_spec_end () + }; + + (void) args; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + + /* validate merchant's wire details (as far as we can) */ + { + char *emsg; + + emsg = TALER_payto_validate (bd->receiver_wire_account); + if (NULL != emsg) + { + MHD_RESULT ret; + + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + emsg); + GNUNET_free (emsg); + return ret; + } + } + if (GNUNET_TIME_timestamp_cmp (bd->refund_deadline, + >, + bd->wire_deadline)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE, + NULL); + } + if (GNUNET_TIME_absolute_is_never (bd->wire_deadline.abs_time)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER, + NULL); + } + TALER_payto_hash (bd->receiver_wire_account, + &bd->wire_target_h_payto); + TALER_merchant_wire_signature_hash (bd->receiver_wire_account, + &bd->wire_salt, + &dc.h_wire); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &dc.accumulated_total_without_fee)); + + /* handle policy, if present */ + if (! dc.has_no_policy) + { + const char *error_hint = NULL; + + if (GNUNET_OK != + TALER_extensions_create_policy_details ( + TEH_currency, + dc.policy_json, + &dc.policy_details, + &error_hint)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED, + error_hint); + } + + TALER_deposit_policy_hash (dc.policy_json, + &dc.h_policy); + } + + bd->num_cdis = json_array_size (coins); + if (0 == bd->num_cdis) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "coins"); + } + if (TALER_MAX_FRESH_COINS < bd->num_cdis) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "coins"); + } + + { + struct TALER_EXCHANGEDB_CoinDepositInformation cdis[ + GNUNET_NZL (bd->num_cdis)]; + struct TALER_Amount deposit_fees[GNUNET_NZL (bd->num_cdis)]; + enum GNUNET_GenericReturnValue res; + unsigned int i; + + bd->cdis = cdis; + dc.deposit_fees = deposit_fees; + for (i = 0; i<bd->num_cdis; i++) + { + struct TALER_Amount amount_without_fee; + + res = parse_coin (connection, + &dc, + json_array_get (coins, + i), + &cdis[i], + &deposit_fees[i]); + if (GNUNET_OK != res) + break; + GNUNET_assert (0 <= + TALER_amount_subtract ( + &amount_without_fee, + &cdis[i].amount_with_fee, + &deposit_fees[i])); + + GNUNET_assert (0 <= + TALER_amount_add ( + &dc.accumulated_total_without_fee, + &dc.accumulated_total_without_fee, + &amount_without_fee)); + } + if (GNUNET_OK != res) + { + for (unsigned int j = 0; j<i; j++) + TALER_denom_sig_free (&cdis[j].coin.denom_sig); + GNUNET_JSON_parse_free (spec); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + + dc.exchange_timestamp = GNUNET_TIME_timestamp_get (); + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure"); + } + + /* execute transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "execute batch deposit", + TEH_MT_REQUEST_BATCH_DEPOSIT, + &mhd_ret, + &batch_deposit_transaction, + &dc)) + { + for (unsigned int j = 0; j<bd->num_cdis; j++) + TALER_denom_sig_free (&cdis[j].coin.denom_sig); + GNUNET_JSON_parse_free (spec); + return mhd_ret; + } + } + + /* generate regular response */ + { + MHD_RESULT mhd_ret; + + mhd_ret = reply_batch_deposit_success (connection, + &dc); + for (unsigned int j = 0; j<bd->num_cdis; j++) + TALER_denom_sig_free (&cdis[j].coin.denom_sig); + GNUNET_JSON_parse_free (spec); + return mhd_ret; + } + } +} + + +/* end of taler-exchange-httpd_batch-deposit.c */ diff --git a/src/exchange/taler-exchange-httpd_deposit.h b/src/exchange/taler-exchange-httpd_batch-deposit.h index a4d598a69..187fb9f20 100644 --- a/src/exchange/taler-exchange-httpd_deposit.h +++ b/src/exchange/taler-exchange-httpd_batch-deposit.h @@ -14,14 +14,14 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-exchange-httpd_deposit.h - * @brief Handle /deposit requests + * @file taler-exchange-httpd_batch-deposit.h + * @brief Handle /batch-deposit requests * @author Florian Dold * @author Benedikt Mueller * @author Christian Grothoff */ -#ifndef TALER_EXCHANGE_HTTPD_DEPOSIT_H -#define TALER_EXCHANGE_HTTPD_DEPOSIT_H +#ifndef TALER_EXCHANGE_HTTPD_BATCH_DEPOSIT_H +#define TALER_EXCHANGE_HTTPD_BATCH_DEPOSIT_H #include <gnunet/gnunet_util_lib.h> #include <microhttpd.h> @@ -29,21 +29,21 @@ /** - * Handle a "/coins/$COIN_PUB/deposit" request. Parses the JSON, and, if + * Handle a "/batch-deposit" request. Parses the JSON, and, if * successful, passes the JSON data to #deposit_transaction() to * further check the details of the operation specified. If everything checks - * out, this will ultimately lead to the "/deposit" being executed, or + * out, this will ultimately lead to the "/batch-deposit" being executed, or * rejected. * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin + * @param rc request context * @param root uploaded JSON data + * @param args arguments, empty in this case * @return MHD result code */ MHD_RESULT -TEH_handler_deposit (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root); +TEH_handler_batch_deposit (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); #endif diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c new file mode 100644 index 000000000..2b80c2fc4 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_batch-withdraw.c @@ -0,0 +1,935 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General + Public License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_batch-withdraw.c + * @brief Handle /reserves/$RESERVE_PUB/batch-withdraw requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler-exchange-httpd.h" +#include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_batch-withdraw.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" +#include "taler_util.h" + + +/** + * Information per planchet in the batch. + */ +struct PlanchetContext +{ + + /** + * Hash of the (blinded) message to be signed by the Exchange. + */ + struct TALER_BlindedCoinHashP h_coin_envelope; + + /** + * Value of the coin being exchanged (matching the denomination key) + * plus the transaction fee. We include this in what is being + * signed so that we can verify a reserve's remaining total balance + * without needing to access the respective denomination key + * information each time. + */ + struct TALER_Amount amount_with_fee; + + /** + * Blinded planchet. + */ + struct TALER_BlindedPlanchet blinded_planchet; + + /** + * Set to the resulting signed coin data to be returned to the client. + */ + struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; + +}; + +/** + * Context for #batch_withdraw_transaction. + */ +struct BatchWithdrawContext +{ + + /** + * Public key of the reserve. + */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /** + * request context + */ + const struct TEH_RequestContext *rc; + + /** + * KYC status of the reserve used for the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Array of @e planchets_length planchets we are processing. + */ + struct PlanchetContext *planchets; + + /** + * Hash of the payto-URI representing the reserve + * from which we are withdrawing. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Current time for the DB transaction. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Total amount from all coins with fees. + */ + struct TALER_Amount batch_total; + + /** + * Length of the @e planchets array. + */ + unsigned int planchets_length; + + /** + * AML decision, #TALER_AML_NORMAL if we may proceed. + */ + enum TALER_AmlDecisionState aml_decision; + +}; + + +/** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and + * account to iterate over events for + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +batch_withdraw_amount_cb (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct BatchWithdrawContext *wc = cls; + enum GNUNET_DB_QueryStatus qs; + + if (GNUNET_OK != + cb (cb_cls, + &wc->batch_total, + wc->now.abs_time)) + return; + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( + TEH_plugin->cls, + &wc->h_payto, + limit, + cb, + cb_cls); + GNUNET_break (qs >= 0); +} + + +/** + * Function called on each @a amount that was found to + * be relevant for the AML check as it was merged into + * the reserve. + * + * @param cls `struct TALER_Amount *` to total up the amounts + * @param amount encountered transaction amount + * @param date when was the amount encountered + * @return #GNUNET_OK to continue to iterate, + * #GNUNET_NO to abort iteration + * #GNUNET_SYSERR on internal error (also abort itaration) + */ +static enum GNUNET_GenericReturnValue +aml_amount_cb ( + void *cls, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Absolute date) +{ + struct TALER_Amount *total = cls; + + GNUNET_assert (0 <= + TALER_amount_add (total, + total, + amount)); + return GNUNET_OK; +} + + +/** + * Generates our final (successful) response. + * + * @param rc request context + * @param wc operation context + * @return MHD queue status + */ +static MHD_RESULT +generate_reply_success (const struct TEH_RequestContext *rc, + const struct BatchWithdrawContext *wc) +{ + json_t *sigs; + + if (! wc->kyc.ok) + { + /* KYC required */ + return TEH_RESPONSE_reply_kyc_required (rc->connection, + &wc->h_payto, + &wc->kyc); + } + if (TALER_AML_NORMAL != wc->aml_decision) + return TEH_RESPONSE_reply_aml_blocked (rc->connection, + wc->aml_decision); + + sigs = json_array (); + GNUNET_assert (NULL != sigs); + for (unsigned int i = 0; i<wc->planchets_length; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + + GNUNET_assert ( + 0 == + json_array_append_new ( + sigs, + GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig ( + "ev_sig", + &pc->collectable.sig)))); + } + TEH_METRICS_batch_withdraw_num_coins += wc->planchets_length; + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + sigs)); +} + + +/** + * Check if the @a wc is replayed and we already have an + * answer. If so, replay the existing answer and return the + * HTTP response. + * + * @param wc parsed request data + * @param[out] mret HTTP status, set if we return true + * @return true if the request is idempotent with an existing request + * false if we did not find the request in the DB and did not set @a mret + */ +static bool +check_request_idempotent (const struct BatchWithdrawContext *wc, + MHD_RESULT *mret) +{ + const struct TEH_RequestContext *rc = wc->rc; + + for (unsigned int i = 0; i<wc->planchets_length; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_withdraw_info (TEH_plugin->cls, + &pc->h_coin_envelope, + &pc->collectable); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mret = TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_withdraw_info"); + return true; /* well, kind-of */ + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return false; + } + /* generate idempotent reply */ + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW]++; + *mret = generate_reply_success (rc, + wc); + return true; +} + + +/** + * Function implementing withdraw transaction. Runs the + * transaction logic; IF it returns a non-error code, the transaction + * logic MUST NOT queue a MHD response. IF it returns an hard error, + * the transaction logic MUST queue a MHD response and set @a mhd_ret. + * IF it returns the soft error code, the function MAY be called again + * to retry and MUST not queue a MHD response. + * + * Note that "wc->collectable.sig" is set before entering this function as we + * signed before entering the transaction. + * + * @param cls a `struct BatchWithdrawContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +batch_withdraw_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct BatchWithdrawContext *wc = cls; + uint64_t ruuid; + enum GNUNET_DB_QueryStatus qs; + bool found = false; + bool balance_ok = false; + bool age_ok = false; + uint16_t allowed_maximum_age = 0; + struct TALER_Amount reserve_balance; + char *kyc_required; + struct TALER_PaytoHashP reserve_h_payto; + + wc->now = GNUNET_TIME_timestamp_get (); + /* Do AML check: compute total merged amount and check + against applicable AML threshold */ + { + char *reserve_payto; + + reserve_payto = TALER_reserve_make_payto (TEH_base_url, + wc->reserve_pub); + TALER_payto_hash (reserve_payto, + &reserve_h_payto); + GNUNET_free (reserve_payto); + } + { + struct TALER_Amount merge_amount; + struct TALER_Amount threshold; + struct GNUNET_TIME_Absolute now_minus_one_month; + + now_minus_one_month + = GNUNET_TIME_absolute_subtract (wc->now.abs_time, + GNUNET_TIME_UNIT_MONTHS); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &merge_amount)); + qs = TEH_plugin->select_merge_amounts_for_kyc_check (TEH_plugin->cls, + &reserve_h_payto, + now_minus_one_month, + &aml_amount_cb, + &merge_amount); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_merge_amounts_for_kyc_check"); + return qs; + } + qs = TEH_plugin->select_aml_threshold (TEH_plugin->cls, + &reserve_h_payto, + &wc->aml_decision, + &wc->kyc, + &threshold); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_aml_threshold"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + threshold = TEH_aml_threshold; /* use default */ + wc->aml_decision = TALER_AML_NORMAL; + } + + switch (wc->aml_decision) + { + case TALER_AML_NORMAL: + if (0 >= TALER_amount_cmp (&merge_amount, + &threshold)) + { + /* merge_amount <= threshold, continue withdraw below */ + break; + } + wc->aml_decision = TALER_AML_PENDING; + qs = TEH_plugin->trigger_aml_process (TEH_plugin->cls, + &reserve_h_payto, + &merge_amount); + if (qs <= 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "trigger_aml_process"); + return qs; + } + return qs; + case TALER_AML_PENDING: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "AML already pending, doing nothing\n"); + return qs; + case TALER_AML_FROZEN: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Account frozen, doing nothing\n"); + return qs; + } + } + + /* Check if the money came from a wire transfer */ + qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls, + wc->reserve_pub, + &wc->h_payto); + if (qs < 0) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "reserves_get_origin"); + return qs; + } + /* If no results, reserve was created by merge, in which case no KYC check + is required as the merge already did that. */ + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, + &wc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &batch_withdraw_amount_cb, + wc, + &kyc_required); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "kyc_test_required"); + return qs; + } + if (NULL != kyc_required) + { + /* insert KYC requirement into DB! */ + wc->kyc.ok = false; + qs = TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + kyc_required, + &wc->h_payto, + wc->reserve_pub, + &wc->kyc.requirement_row); + GNUNET_free (kyc_required); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_for_account"); + } + return qs; + } + } + wc->kyc.ok = true; + + qs = TEH_plugin->do_batch_withdraw (TEH_plugin->cls, + wc->now, + wc->reserve_pub, + &wc->batch_total, + TEH_age_restriction_enabled, + &found, + &balance_ok, + &reserve_balance, + &age_ok, + &allowed_maximum_age, + &ruuid); + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "update_reserve_batch_withdraw"); + } + return qs; + } + if (! found) + { + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (! age_ok) + { + /* We respond with the lowest age in the corresponding age group + * of the required age */ + uint16_t lowest_age = TALER_get_lowest_age ( + &TEH_age_restriction_config.mask, + allowed_maximum_age); + + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret = TEH_RESPONSE_reply_reserve_age_restriction_required ( + connection, + lowest_age); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (! balance_ok) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( + connection, + TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, + &reserve_balance, + &wc->batch_total, + wc->reserve_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + /* Add information about each planchet in the batch */ + for (unsigned int i = 0; i<wc->planchets_length; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet; + const union GNUNET_CRYPTO_BlindSessionNonce *nonce = NULL; + bool denom_unknown = true; + bool conflict = true; + bool nonce_reuse = true; + + switch (bp->blinded_message->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + break; + case GNUNET_CRYPTO_BSA_RSA: + break; + case GNUNET_CRYPTO_BSA_CS: + nonce = (const union GNUNET_CRYPTO_BlindSessionNonce *) + &bp->blinded_message->details.cs_blinded_message.nonce; + break; + } + qs = TEH_plugin->do_batch_withdraw_insert (TEH_plugin->cls, + nonce, + &pc->collectable, + wc->now, + ruuid, + &denom_unknown, + &conflict, + &nonce_reuse); + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_batch_withdraw_insert"); + return qs; + } + if (denom_unknown) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (conflict) ) + { + if (! check_request_idempotent (wc, + mhd_ret)) + { + /* We do not support *some* of the coins of the request being + idempotent while others being fresh. */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Idempotent coin in batch, not allowed. Aborting.\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET, + NULL); + } + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (nonce_reuse) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW]++; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * The request was parsed successfully. Prepare + * our side for the main DB transaction. + * + * @param rc request details + * @param wc storage for request processing + * @return MHD result for the @a rc + */ +static MHD_RESULT +prepare_transaction (const struct TEH_RequestContext *rc, + struct BatchWithdrawContext *wc) +{ + struct TEH_CoinSignData csds[wc->planchets_length]; + struct TALER_BlindedDenominationSignature bss[wc->planchets_length]; + + for (unsigned int i = 0; i<wc->planchets_length; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + + csds[i].h_denom_pub = &pc->collectable.denom_pub_hash; + csds[i].bp = &pc->blinded_planchet; + } + { + enum TALER_ErrorCode ec; + + ec = TEH_keys_denomination_batch_sign ( + wc->planchets_length, + csds, + false, + bss); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); + } + } + for (unsigned int i = 0; i<wc->planchets_length; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + + pc->collectable.sig = bss[i]; + } + + /* run transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "run batch withdraw", + TEH_MT_REQUEST_WITHDRAW, + &mhd_ret, + &batch_withdraw_transaction, + wc)) + { + return mhd_ret; + } + } + /* return final positive response */ + return generate_reply_success (rc, + wc); +} + + +/** + * Continue processing the request @a rc by parsing the + * @a planchets and then running the transaction. + * + * @param rc request details + * @param wc storage for request processing + * @param planchets array of planchets to parse + * @return MHD result for the @a rc + */ +static MHD_RESULT +parse_planchets (const struct TEH_RequestContext *rc, + struct BatchWithdrawContext *wc, + const json_t *planchets) +{ + struct TEH_KeyStateHandle *ksh; + MHD_RESULT mret; + + for (unsigned int i = 0; i<wc->planchets_length; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &pc->collectable.reserve_sig), + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &pc->collectable.denom_pub_hash), + TALER_JSON_spec_blinded_planchet ("coin_ev", + &pc->blinded_planchet), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + json_array_get (planchets, + i), + ispec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + pc->collectable.reserve_pub = *wc->reserve_pub; + for (unsigned int k = 0; k<i; k++) + { + const struct PlanchetContext *kpc = &wc->planchets[k]; + + if (0 == + TALER_blinded_planchet_cmp (&kpc->blinded_planchet, + &pc->blinded_planchet)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate planchet"); + } + } + } + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + if (! check_request_idempotent (wc, + &mret)) + { + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + return mret; + } + for (unsigned int i = 0; i<wc->planchets_length; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + struct TEH_DenominationKey *dk; + + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + &pc->collectable.denom_pub_hash, + NULL, + NULL); + + if (NULL == dk) + { + if (! check_request_idempotent (wc, + &mret)) + { + return TEH_RESPONSE_reply_unknown_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash); + } + return mret; + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) + { + /* This denomination is past the expiration time for withdraws */ + if (! check_request_idempotent (wc, + &mret)) + { + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "WITHDRAW"); + } + return mret; + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid, no need to check + for idempotency! */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "WITHDRAW"); + } + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + if (! check_request_idempotent (wc, + &mret)) + { + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "WITHDRAW"); + } + return mret; + } + if (dk->denom_pub.bsign_pub_key->cipher != + pc->blinded_planchet.blinded_message->cipher) + { + /* denomination cipher and blinded planchet cipher not the same */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL); + } + if (0 > + TALER_amount_add (&pc->collectable.amount_with_fee, + &dk->meta.value, + &dk->meta.fees.withdraw)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + NULL); + } + if (0 > + TALER_amount_add (&wc->batch_total, + &wc->batch_total, + &pc->collectable.amount_with_fee)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + NULL); + } + + TALER_coin_ev_hash (&pc->blinded_planchet, + &pc->collectable.denom_pub_hash, + &pc->collectable.h_coin_envelope); + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_withdraw_verify (&pc->collectable.denom_pub_hash, + &pc->collectable.amount_with_fee, + &pc->collectable.h_coin_envelope, + &pc->collectable.reserve_pub, + &pc->collectable.reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, + NULL); + } + } + /* everything parsed */ + return prepare_transaction (rc, + wc); +} + + +MHD_RESULT +TEH_handler_batch_withdraw (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct BatchWithdrawContext wc = { + .reserve_pub = reserve_pub, + .rc = rc + }; + const json_t *planchets; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("planchets", + &planchets), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &wc.batch_total)); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + wc.planchets_length = json_array_size (planchets); + if (0 == wc.planchets_length) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "planchets"); + } + if (wc.planchets_length > TALER_MAX_FRESH_COINS) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "too many planchets"); + } + { + struct PlanchetContext splanchets[wc.planchets_length]; + MHD_RESULT ret; + + memset (splanchets, + 0, + sizeof (splanchets)); + wc.planchets = splanchets; + ret = parse_planchets (rc, + &wc, + planchets); + /* Clean up */ + for (unsigned int i = 0; i<wc.planchets_length; i++) + { + struct PlanchetContext *pc = &wc.planchets[i]; + + TALER_blinded_planchet_free (&pc->blinded_planchet); + TALER_blinded_denom_sig_free (&pc->collectable.sig); + } + return ret; + } +} + + +/* end of taler-exchange-httpd_batch-withdraw.c */ diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.h b/src/exchange/taler-exchange-httpd_batch-withdraw.h new file mode 100644 index 000000000..dfc6e5ad8 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_batch-withdraw.h @@ -0,0 +1,48 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_batch-withdraw.h + * @brief Handle /reserve/batch-withdraw requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H +#define TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/reserves/$RESERVE_PUB/batch-withdraw" request. Parses the batch of + * requested "denom_pub" which specifies the key/value of the coin to be + * withdrawn, and checks that the signature "reserve_sig" makes this a valid + * withdrawal request from the specified reserve. If so, the envelope with + * the blinded coin "coin_ev" is passed down to execute the withdrawal + * operation. + * + * @param rc request context + * @param root uploaded JSON data + * @param reserve_pub public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_batch_withdraw (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_coins_get.c b/src/exchange/taler-exchange-httpd_coins_get.c new file mode 100644 index 000000000..cd453275e --- /dev/null +++ b/src/exchange/taler-exchange-httpd_coins_get.c @@ -0,0 +1,709 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_coins_get.c + * @brief Handle GET /coins/$COIN_PUB/history requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_mhd_lib.h" +#include "taler_json_lib.h" +#include "taler_dbevents.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_coins_get.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Add the headers we want to set for every response. + * + * @param cls the key state to use + * @param[in,out] response the response to modify + */ +static void +add_response_headers (void *cls, + struct MHD_Response *response) +{ + (void) cls; + TALER_MHD_add_global_headers (response); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-cache")); +} + + +/** + * Compile the transaction history of a coin into a JSON object. + * + * @param coin_pub public key of the coin + * @param tl transaction history to JSON-ify + * @return json representation of the @a rh, NULL on error + */ +static json_t * +compile_transaction_history ( + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_EXCHANGEDB_TransactionList *tl) +{ + json_t *history; + + history = json_array (); + if (NULL == history) + { + GNUNET_break (0); /* out of memory!? */ + return NULL; + } + for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; + NULL != pos; + pos = pos->next) + { + switch (pos->type) + { + case TALER_EXCHANGEDB_TT_DEPOSIT: + { + const struct TALER_EXCHANGEDB_DepositListEntry *deposit = + pos->details.deposit; + struct TALER_MerchantWireHashP h_wire; + + TALER_merchant_wire_signature_hash (deposit->receiver_wire_account, + &deposit->wire_salt, + &h_wire); +#if ENABLE_SANITY_CHECKS + /* internal sanity check before we hand out a bogus sig... */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_deposit_verify ( + &deposit->amount_with_fee, + &deposit->deposit_fee, + &h_wire, + &deposit->h_contract_terms, + deposit->no_wallet_data_hash + ? NULL + : &deposit->wallet_data_hash, + deposit->no_age_commitment + ? NULL + : &deposit->h_age_commitment, + &deposit->h_policy, + &deposit->h_denom_pub, + deposit->timestamp, + &deposit->merchant_pub, + deposit->refund_deadline, + coin_pub, + &deposit->csig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } +#endif + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "DEPOSIT"), + TALER_JSON_pack_amount ("amount", + &deposit->amount_with_fee), + TALER_JSON_pack_amount ("deposit_fee", + &deposit->deposit_fee), + GNUNET_JSON_pack_timestamp ("timestamp", + deposit->timestamp), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("refund_deadline", + deposit->refund_deadline)), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &deposit->merchant_pub), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &deposit->h_contract_terms), + GNUNET_JSON_pack_data_auto ("h_wire", + &h_wire), + GNUNET_JSON_pack_allow_null ( + deposit->no_age_commitment ? + GNUNET_JSON_pack_string ( + "h_age_commitment", NULL) : + GNUNET_JSON_pack_data_auto ("h_age_commitment", + &deposit->h_age_commitment)), + GNUNET_JSON_pack_data_auto ("coin_sig", + &deposit->csig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + case TALER_EXCHANGEDB_TT_MELT: + { + const struct TALER_EXCHANGEDB_MeltListEntry *melt = + pos->details.melt; + const struct TALER_AgeCommitmentHash *phac = NULL; + +#if ENABLE_SANITY_CHECKS + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_melt_verify ( + &melt->amount_with_fee, + &melt->melt_fee, + &melt->rc, + &melt->h_denom_pub, + &melt->h_age_commitment, + coin_pub, + &melt->coin_sig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } +#endif + + /* Age restriction is optional. We communicate a NULL value to + * JSON_PACK below */ + if (! melt->no_age_commitment) + phac = &melt->h_age_commitment; + + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "MELT"), + TALER_JSON_pack_amount ("amount", + &melt->amount_with_fee), + TALER_JSON_pack_amount ("melt_fee", + &melt->melt_fee), + GNUNET_JSON_pack_data_auto ("rc", + &melt->rc), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + phac)), + GNUNET_JSON_pack_data_auto ("coin_sig", + &melt->coin_sig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_TT_REFUND: + { + const struct TALER_EXCHANGEDB_RefundListEntry *refund = + pos->details.refund; + struct TALER_Amount value; + +#if ENABLE_SANITY_CHECKS + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_merchant_refund_verify ( + coin_pub, + &refund->h_contract_terms, + refund->rtransaction_id, + &refund->refund_amount, + &refund->merchant_pub, + &refund->merchant_sig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } +#endif + if (0 > + TALER_amount_subtract (&value, + &refund->refund_amount, + &refund->refund_fee)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "REFUND"), + TALER_JSON_pack_amount ("amount", + &value), + TALER_JSON_pack_amount ("refund_fee", + &refund->refund_fee), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &refund->h_contract_terms), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &refund->merchant_pub), + GNUNET_JSON_pack_uint64 ("rtransaction_id", + refund->rtransaction_id), + GNUNET_JSON_pack_data_auto ("merchant_sig", + &refund->merchant_sig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + { + struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = + pos->details.old_coin_recoup; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_refresh_sign ( + &TEH_keys_exchange_sign_, + pr->timestamp, + &pr->value, + &pr->coin.coin_pub, + &pr->old_coin_pub, + &epub, + &esig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + /* NOTE: we could also provide coin_pub's coin_sig, denomination key hash and + the denomination key's RSA signature over coin_pub, but as the + wallet should really already have this information (and cannot + check or do anything with it anyway if it doesn't), it seems + strictly unnecessary. */ + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "OLD-COIN-RECOUP"), + TALER_JSON_pack_amount ("amount", + &pr->value), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("coin_pub", + &pr->coin.coin_pub), + GNUNET_JSON_pack_timestamp ("timestamp", + pr->timestamp)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + case TALER_EXCHANGEDB_TT_RECOUP: + { + const struct TALER_EXCHANGEDB_RecoupListEntry *recoup = + pos->details.recoup; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_sign ( + &TEH_keys_exchange_sign_, + recoup->timestamp, + &recoup->value, + coin_pub, + &recoup->reserve_pub, + &epub, + &esig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RECOUP"), + TALER_JSON_pack_amount ("amount", + &recoup->value), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &recoup->reserve_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &recoup->coin_sig), + GNUNET_JSON_pack_data_auto ("coin_blind", + &recoup->coin_blind), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &recoup->reserve_pub), + GNUNET_JSON_pack_timestamp ("timestamp", + recoup->timestamp)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: + { + struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = + pos->details.recoup_refresh; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_refresh_sign ( + &TEH_keys_exchange_sign_, + pr->timestamp, + &pr->value, + coin_pub, + &pr->old_coin_pub, + &epub, + &esig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + /* NOTE: we could also provide coin_pub's coin_sig, denomination key + hash and the denomination key's RSA signature over coin_pub, but as + the wallet should really already have this information (and cannot + check or do anything with it anyway if it doesn't), it seems + strictly unnecessary. */ + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RECOUP-REFRESH"), + TALER_JSON_pack_amount ("amount", + &pr->value), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("old_coin_pub", + &pr->old_coin_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &pr->coin_sig), + GNUNET_JSON_pack_data_auto ("coin_blind", + &pr->coin_blind), + GNUNET_JSON_pack_timestamp ("timestamp", + pr->timestamp)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + + case TALER_EXCHANGEDB_TT_PURSE_DEPOSIT: + { + struct TALER_EXCHANGEDB_PurseDepositListEntry *pd + = pos->details.purse_deposit; + const struct TALER_AgeCommitmentHash *phac = NULL; + + if (! pd->no_age_commitment) + phac = &pd->h_age_commitment; + + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "PURSE-DEPOSIT"), + TALER_JSON_pack_amount ("amount", + &pd->amount), + GNUNET_JSON_pack_string ("exchange_base_url", + NULL == pd->exchange_base_url + ? TEH_base_url + : pd->exchange_base_url), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + phac)), + GNUNET_JSON_pack_data_auto ("purse_pub", + &pd->purse_pub), + GNUNET_JSON_pack_bool ("refunded", + pd->refunded), + GNUNET_JSON_pack_data_auto ("coin_sig", + &pd->coin_sig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + + case TALER_EXCHANGEDB_TT_PURSE_REFUND: + { + const struct TALER_EXCHANGEDB_PurseRefundListEntry *prefund = + pos->details.purse_refund; + struct TALER_Amount value; + enum TALER_ErrorCode ec; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (0 > + TALER_amount_subtract (&value, + &prefund->refund_amount, + &prefund->refund_fee)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + ec = TALER_exchange_online_purse_refund_sign ( + &TEH_keys_exchange_sign_, + &value, + &prefund->refund_fee, + coin_pub, + &prefund->purse_pub, + &epub, + &esig); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "PURSE-REFUND"), + TALER_JSON_pack_amount ("amount", + &value), + TALER_JSON_pack_amount ("refund_fee", + &prefund->refund_fee), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("purse_pub", + &prefund->purse_pub)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + + case TALER_EXCHANGEDB_TT_RESERVE_OPEN: + { + struct TALER_EXCHANGEDB_ReserveOpenListEntry *role + = pos->details.reserve_open; + + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RESERVE-OPEN-DEPOSIT"), + TALER_JSON_pack_amount ("coin_contribution", + &role->coin_contribution), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &role->reserve_sig), + GNUNET_JSON_pack_data_auto ("coin_sig", + &role->coin_sig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + } + } + return history; +} + + +MHD_RESULT +TEH_handler_coins_get (struct TEH_RequestContext *rc, + const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + struct TALER_EXCHANGEDB_TransactionList *tl = NULL; + uint64_t start_off = 0; + uint64_t etag_in; + uint64_t etag_out; + char etagp[24]; + struct MHD_Response *resp; + unsigned int http_status; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_Amount balance; + + TALER_MHD_parse_request_number (rc->connection, + "start", + &start_off); + /* Check signature */ + { + struct TALER_CoinSpendSignatureP coin_sig; + bool required = true; + + TALER_MHD_parse_request_header_auto (rc->connection, + TALER_COIN_HISTORY_SIGNATURE_HEADER, + &coin_sig, + required); + if (GNUNET_OK != + TALER_wallet_coin_history_verify (start_off, + coin_pub, + &coin_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_COIN_HISTORY_BAD_SIGNATURE, + NULL); + } + } + + /* Get etag */ + { + const char *etags; + + etags = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != etags) + { + char dummy; + unsigned long long ev; + + if (1 != sscanf (etags, + "\"%llu\"%c", + &ev, + &dummy)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client send malformed `If-None-Match' header `%s'\n", + etags); + etag_in = start_off; + } + else + { + etag_in = (uint64_t) ev; + } + } + else + { + etag_in = start_off; + } + } + + /* Get history from DB between etag and now */ + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, + coin_pub, + start_off, + etag_in, + &etag_out, + &balance, + &h_denom_pub, + &tl); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_coin_history"); + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_coin_history"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Handled below */ + break; + } + } + + GNUNET_snprintf (etagp, + sizeof (etagp), + "\"%llu\"", + (unsigned long long) etag_out); + if (etag_in == etag_out) + { + return TEH_RESPONSE_reply_not_modified (rc->connection, + etagp, + &add_response_headers, + NULL); + } + if (NULL == tl) + { + /* 204: empty history */ + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + http_status = MHD_HTTP_NO_CONTENT; + } + else + { + /* 200: regular history */ + json_t *history; + + history = compile_transaction_history (coin_pub, + tl); + TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, + tl); + tl = NULL; + if (NULL == history) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + "Failed to compile coin history"); + } + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &h_denom_pub), + TALER_JSON_pack_amount ("balance", + &balance), + GNUNET_JSON_pack_array_steal ("history", + history)); + http_status = MHD_HTTP_OK; + } + add_response_headers (NULL, + resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etagp)); + { + MHD_RESULT ret; + + ret = MHD_queue_response (rc->connection, + http_status, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } +} + + +/* end of taler-exchange-httpd_coins_get.c */ diff --git a/src/exchange/taler-exchange-httpd_coins_get.h b/src/exchange/taler-exchange-httpd_coins_get.h new file mode 100644 index 000000000..90405b55d --- /dev/null +++ b/src/exchange/taler-exchange-httpd_coins_get.h @@ -0,0 +1,53 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_coins_get.h + * @brief Handle GET /coins/$COIN_PUB requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_COINS_GET_H +#define TALER_EXCHANGE_HTTPD_COINS_GET_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown reserves-get subsystem. Resumes all + * suspended long-polling clients and cleans up + * data structures. + */ +void +TEH_reserves_get_cleanup (void); + + +/** + * Handle a GET "/coins/$COIN_PUB/history" request. Parses the + * given "coins_pub" in @a args (which should contain the + * EdDSA public key of a reserve) and then respond with the + * transaction history of the coin. + * + * @param rc request context + * @param coin_pub public key of the coin + * @return MHD result code + */ +MHD_RESULT +TEH_handler_coins_get (struct TEH_RequestContext *rc, + const struct TALER_CoinSpendPublicKeyP *coin_pub); + +#endif diff --git a/src/exchange/taler-exchange-httpd_common_deposit.c b/src/exchange/taler-exchange-httpd_common_deposit.c new file mode 100644 index 000000000..898e23dd9 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_common_deposit.c @@ -0,0 +1,268 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_common_deposit.c + * @brief shared logic for handling deposited coins + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd.h" +#include "taler-exchange-httpd_keys.h" + + +enum GNUNET_GenericReturnValue +TEH_common_purse_deposit_parse_coin ( + struct MHD_Connection *connection, + struct TEH_PurseDepositedCoin *coin, + const json_t *jcoin) +{ + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("amount", + TEH_currency, + &coin->amount), + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &coin->cpi.denom_pub_hash), + TALER_JSON_spec_denom_sig ("ub_sig", + &coin->cpi.denom_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("attest", + &coin->attest), + &coin->no_attest), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_age_commitment ("age_commitment", + &coin->age_commitment), + &coin->cpi.no_age_commitment), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &coin->coin_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &coin->cpi.coin_pub), + GNUNET_JSON_spec_end () + }; + + memset (coin, + 0, + sizeof (*coin)); + coin->cpi.no_age_commitment = true; + coin->no_attest = true; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + jcoin, + spec); + if (GNUNET_OK != res) + return res; + } + + /* check denomination exists and is valid */ + { + struct TEH_DenominationKey *dk; + MHD_RESULT mret; + + dk = TEH_keys_denomination_by_hash (&coin->cpi.denom_pub_hash, + connection, + &mret); + if (NULL == dk) + { + GNUNET_JSON_parse_free (spec); + return (MHD_YES == mret) ? GNUNET_NO : GNUNET_SYSERR; + } + if (! coin->cpi.no_age_commitment) + { + coin->age_commitment.mask = dk->meta.age_mask; + TALER_age_commitment_hash (&coin->age_commitment, + &coin->cpi.h_age_commitment); + } + if (0 > TALER_amount_cmp (&dk->meta.value, + &coin->amount)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE, + NULL)) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + { + /* This denomination is past the expiration time for deposits */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->cpi.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "PURSE CREATE")) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->cpi.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "PURSE CREATE")) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->cpi.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "PURSE CREATE")) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (dk->denom_pub.bsign_pub_key->cipher != + coin->cpi.denom_sig.unblinded_sig->cipher) + { + /* denomination cipher and denomination signature cipher not the same */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)) + ? GNUNET_NO : GNUNET_SYSERR; + } + + coin->deposit_fee = dk->meta.fees.deposit; + if (0 < TALER_amount_cmp (&coin->deposit_fee, + &coin->amount)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE, + NULL); + } + GNUNET_assert (0 <= + TALER_amount_subtract (&coin->amount_minus_fee, + &coin->amount, + &coin->deposit_fee)); + + /* check coin signature */ + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } + if (GNUNET_YES != + TALER_test_coin_valid (&coin->cpi, + &dk->denom_pub)) + { + TALER_LOG_WARNING ("Invalid coin passed for /deposit\n"); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)) + ? GNUNET_NO : GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TEH_common_deposit_check_purse_deposit ( + struct MHD_Connection *connection, + const struct TEH_PurseDepositedCoin *coin, + const struct TALER_PurseContractPublicKeyP *purse_pub, + uint32_t min_age) +{ + if (GNUNET_OK != + TALER_wallet_purse_deposit_verify (TEH_base_url, + purse_pub, + &coin->amount, + &coin->cpi.denom_pub_hash, + &coin->cpi.h_age_commitment, + &coin->cpi.coin_pub, + &coin->coin_sig)) + { + TALER_LOG_WARNING ( + "Invalid coin signature to deposit into purse\n"); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID, + TEH_base_url)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + if (0 == min_age) + return GNUNET_OK; /* no need to apply age checks */ + + /* Check and verify the age restriction. */ + if (coin->no_attest != coin->cpi.no_age_commitment) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT, + "mismatch of attest and age_commitment"); + } + + if (coin->cpi.no_age_commitment) + return GNUNET_OK; /* unrestricted coin */ + + /* age attestation must be valid */ + if (GNUNET_OK != + TALER_age_commitment_verify (&coin->age_commitment, + min_age, + &coin->attest)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE, + "invalid attest for minimum age"); + } + return GNUNET_OK; +} + + +/** + * Release data structures of @a coin. Note that + * @a coin itself is NOT freed. + * + * @param[in] coin information to release + */ +void +TEH_common_purse_deposit_free_coin (struct TEH_PurseDepositedCoin *coin) +{ + TALER_denom_sig_free (&coin->cpi.denom_sig); + if (! coin->cpi.no_age_commitment) + GNUNET_free (coin->age_commitment.keys); /* Only the keys have been allocated */ +} diff --git a/src/exchange/taler-exchange-httpd_common_deposit.h b/src/exchange/taler-exchange-httpd_common_deposit.h new file mode 100644 index 000000000..10fd7e8bf --- /dev/null +++ b/src/exchange/taler-exchange-httpd_common_deposit.h @@ -0,0 +1,130 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_common_deposit.h + * @brief shared logic for handling deposited coins + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_COMMON_DEPOSIT_H +#define TALER_EXCHANGE_HTTPD_COMMON_DEPOSIT_H + +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" + + +/** + * Information about an individual coin being deposited. + */ +struct TEH_PurseDepositedCoin +{ + /** + * Public information about the coin. + */ + struct TALER_CoinPublicInfo cpi; + + /** + * Signature affirming spending the coin. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Amount to be put into the purse from this coin. + */ + struct TALER_Amount amount; + + /** + * Deposit fee applicable for this coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Amount to be put into the purse from this coin. + */ + struct TALER_Amount amount_minus_fee; + + /** + * Age attestation provided, set if @e no_attest is false. + */ + struct TALER_AgeAttestation attest; + + /** + * Age commitment provided, set if @e cpi.no_age_commitment is false. + */ + struct TALER_AgeCommitment age_commitment; + + /** + * ID of the coin in known_coins. + */ + uint64_t known_coin_id; + + /** + * True if @e attest was not provided. + */ + bool no_attest; + +}; + + +/** + * Parse a coin and check signature of the coin and the denomination + * signature over the coin. + * + * @param[in,out] connection our HTTP connection + * @param[out] coin coin to initialize + * @param jcoin coin to parse + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +enum GNUNET_GenericReturnValue +TEH_common_purse_deposit_parse_coin ( + struct MHD_Connection *connection, + struct TEH_PurseDepositedCoin *coin, + const json_t *jcoin); + + +/** + * Check that the deposited @a coin is valid for @a purse_pub + * and has a valid age commitment for @a min_age. + * + * @param[in,out] connection our HTTP connection + * @param coin the coin to evaluate + * @param purse_pub public key of the purse the coin was deposited into + * @param min_age minimum age restriction expected for this purse + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +enum GNUNET_GenericReturnValue +TEH_common_deposit_check_purse_deposit ( + struct MHD_Connection *connection, + const struct TEH_PurseDepositedCoin *coin, + const struct TALER_PurseContractPublicKeyP *purse_pub, + uint32_t min_age); + + +/** + * Release data structures of @a coin. Note that + * @a coin itself is NOT freed. + * + * @param[in] coin information to release + */ +void +TEH_common_purse_deposit_free_coin (struct TEH_PurseDepositedCoin *coin); + +#endif diff --git a/src/exchange/taler-exchange-httpd_common_kyc.c b/src/exchange/taler-exchange-httpd_common_kyc.c new file mode 100644 index 000000000..bcee5a0d2 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_common_kyc.c @@ -0,0 +1,302 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_common_kyc.c + * @brief shared logic for finishing a KYC process + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-exchange-httpd.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler_attributes.h" +#include "taler_error_codes.h" +#include "taler_kyclogic_lib.h" +#include "taler_exchangedb_plugin.h" +#include <gnunet/gnunet_common.h> + +struct TEH_KycAmlTrigger +{ + + /** + * Our logging scope. + */ + struct GNUNET_AsyncScopeId scope; + + /** + * account the operation is about + */ + struct TALER_PaytoHashP account_id; + + /** + * until when is the KYC data valid + */ + struct GNUNET_TIME_Absolute expiration; + + /** + * legitimization process the KYC data is about + */ + uint64_t process_row; + + /** + * name of the configuration section of the logic that was run + */ + char *provider_section; + + /** + * set to user ID at the provider, or NULL if not supported or unknown + */ + char *provider_user_id; + + /** + * provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + */ + char *provider_legitimization_id; + + /** + * function to call with the result + */ + TEH_KycAmlTriggerCallback cb; + + /** + * closure for @e cb + */ + void *cb_cls; + + /** + * user attributes returned by the provider + */ + json_t *attributes; + + /** + * response to return to the HTTP client + */ + struct MHD_Response *response; + + /** + * Handle to an external process that evaluates the + * need to run AML on the account. + */ + struct TALER_JSON_ExternalConversion *kyc_aml; + + /** + * HTTP status code of @e response + */ + unsigned int http_status; + +}; + + +/** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure of type `struct TEH_KycAmlTrigger *` + * @param status_type how did the process die + * @param code termination status code from the process, + * non-zero if AML checks are required next + * @param result some JSON result, NULL if we failed to get an JSON output + */ +static void +kyc_aml_finished (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *result) +{ + struct TEH_KycAmlTrigger *kat = cls; + enum GNUNET_DB_QueryStatus qs; + size_t eas; + void *ea; + const char *birthdate; + unsigned int birthday = 0; + struct GNUNET_ShortHashCode kyc_prox; + struct GNUNET_AsyncScopeSave old_scope; + unsigned int num_checks; + char **provided_checks; + + kat->kyc_aml = NULL; + GNUNET_async_scope_enter (&kat->scope, + &old_scope); + TALER_CRYPTO_attributes_to_kyc_prox (kat->attributes, + &kyc_prox); + birthdate = json_string_value (json_object_get (kat->attributes, + TALER_ATTRIBUTE_BIRTHDATE)); + if ( (TEH_age_restriction_enabled) && + (NULL != birthdate) ) + { + enum GNUNET_GenericReturnValue ret; + + ret = TALER_parse_coarse_date (birthdate, + &TEH_age_restriction_config.mask, + &birthday); + + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse birthdate `%s' from KYC attributes\n", + birthdate); + if (NULL != kat->response) + MHD_destroy_response (kat->response); + kat->http_status = MHD_HTTP_BAD_REQUEST; + kat->response = TALER_MHD_make_error ( + TALER_EC_GENERIC_PARAMETER_MALFORMED, + TALER_ATTRIBUTE_BIRTHDATE); + goto RETURN_RESULT; + } + } + + TALER_CRYPTO_kyc_attributes_encrypt (&TEH_attribute_key, + kat->attributes, + &ea, + &eas); + TALER_KYCLOGIC_lookup_checks (kat->provider_section, + &num_checks, + &provided_checks); + qs = TEH_plugin->insert_kyc_attributes ( + TEH_plugin->cls, + kat->process_row, + &kat->account_id, + &kyc_prox, + kat->provider_section, + num_checks, + (const char **) provided_checks, + birthday, + GNUNET_TIME_timestamp_get (), + kat->provider_user_id, + kat->provider_legitimization_id, + kat->expiration, + eas, + ea, + 0 != code); + for (unsigned int i = 0; i<num_checks; i++) + GNUNET_free (provided_checks[i]); + GNUNET_free (provided_checks); + GNUNET_free (ea); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Stored encrypted KYC process #%llu attributes: %d\n", + (unsigned long long) kat->process_row, + qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + if (NULL != kat->response) + MHD_destroy_response (kat->response); + kat->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + kat->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "do_insert_kyc_attributes"); + /* Continued below to return the response */ + } +RETURN_RESULT: + /* Finally, return result to main handler */ + kat->cb (kat->cb_cls, + kat->http_status, + kat->response); + kat->response = NULL; + TEH_kyc_finished_cancel (kat); + GNUNET_async_scope_restore (&old_scope); +} + + +struct TEH_KycAmlTrigger * +TEH_kyc_finished (const struct GNUNET_AsyncScopeId *scope, + uint64_t process_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_section, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response, + TEH_KycAmlTriggerCallback cb, + void *cb_cls) +{ + struct TEH_KycAmlTrigger *kat; + + kat = GNUNET_new (struct TEH_KycAmlTrigger); + kat->scope = *scope; + kat->process_row = process_row; + kat->account_id = *account_id; + kat->provider_section + = GNUNET_strdup (provider_section); + if (NULL != provider_user_id) + kat->provider_user_id + = GNUNET_strdup (provider_user_id); + if (NULL != provider_legitimization_id) + kat->provider_legitimization_id + = GNUNET_strdup (provider_legitimization_id); + kat->expiration = expiration; + kat->attributes = json_incref ((json_t*) attributes); + kat->http_status = http_status; + kat->response = response; + kat->cb = cb; + kat->cb_cls = cb_cls; + kat->kyc_aml + = TALER_JSON_external_conversion_start ( + attributes, + &kyc_aml_finished, + kat, + TEH_kyc_aml_trigger, + TEH_kyc_aml_trigger, + NULL); + if (NULL == kat->kyc_aml) + { + GNUNET_break (0); + TEH_kyc_finished_cancel (kat); + return NULL; + } + return kat; +} + + +void +TEH_kyc_finished_cancel (struct TEH_KycAmlTrigger *kat) +{ + if (NULL != kat->kyc_aml) + { + TALER_JSON_external_conversion_stop (kat->kyc_aml); + kat->kyc_aml = NULL; + } + GNUNET_free (kat->provider_section); + GNUNET_free (kat->provider_user_id); + GNUNET_free (kat->provider_legitimization_id); + json_decref (kat->attributes); + if (NULL != kat->response) + { + MHD_destroy_response (kat->response); + kat->response = NULL; + } + GNUNET_free (kat); +} + + +bool +TEH_kyc_failed (uint64_t process_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_section, + const char *provider_user_id, + const char *provider_legitimization_id) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->insert_kyc_failure ( + TEH_plugin->cls, + process_row, + account_id, + provider_section, + provider_user_id, + provider_legitimization_id); + GNUNET_break (qs >= 0); + return qs >= 0; +} diff --git a/src/exchange/taler-exchange-httpd_common_kyc.h b/src/exchange/taler-exchange-httpd_common_kyc.h new file mode 100644 index 000000000..8198679c9 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_common_kyc.h @@ -0,0 +1,117 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_common_kyc.h + * @brief shared logic for finishing a KYC process + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_COMMON_KYC_H +#define TALER_EXCHANGE_HTTPD_COMMON_KYC_H + +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd.h" + + +/** + * Function called after the KYC-AML trigger is done. + * + * @param cls closure + * @param http_status final HTTP status to return + * @param[in] response final HTTP ro return + */ +typedef void +(*TEH_KycAmlTriggerCallback) ( + void *cls, + unsigned int http_status, + struct MHD_Response *response); + + +/** + * Handle for an asynchronous operation to finish + * a KYC process after running the AML trigger. + */ +struct TEH_KycAmlTrigger; + +// FIXME: also pass async log context and set it! +/** + * We have finished a KYC process and obtained new + * @a attributes for a given @a account_id. + * Check with the KYC-AML trigger to see if we need + * to initiate an AML process, and store the attributes + * in the database. Then call @a cb. + * + * @param scope the HTTP request logging scope + * @param process_row legitimization process the webhook was about + * @param account_id account the webhook was about + * @param provider_section name of the configuration section of the logic that was run + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel the operation + */ +struct TEH_KycAmlTrigger * +TEH_kyc_finished (const struct GNUNET_AsyncScopeId *scope, + uint64_t process_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_section, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response, + TEH_KycAmlTriggerCallback cb, + void *cb_cls); + + +/** + * Cancel KYC finish operation. + * + * @param[in] kat operation to abort + */ +void +TEH_kyc_finished_cancel (struct TEH_KycAmlTrigger *kat); + + +/** + * Update state of a legitmization process to 'finished' + * (and failed, no attributes were obtained). + * + * @param process_row legitimization process the webhook was about + * @param account_id account the webhook was about + * @param provider_section name of the configuration section of the logic that was run + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @return true on success, false if updating the database failed + */ +bool +TEH_kyc_failed (uint64_t process_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_section, + const char *provider_user_id, + const char *provider_legitimization_id); + +#endif diff --git a/src/exchange/taler-exchange-httpd_config.c b/src/exchange/taler-exchange-httpd_config.c new file mode 100644 index 000000000..257dfa6ba --- /dev/null +++ b/src/exchange/taler-exchange-httpd_config.c @@ -0,0 +1,92 @@ +/* + This file is part of TALER + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_config.c + * @brief Handle /config requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_json_lib.h> +#include "taler_dbevents.h" +#include "taler-exchange-httpd_config.h" +#include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include <jansson.h> + + +MHD_RESULT +TEH_handler_config (struct TEH_RequestContext *rc, + const char *const args[]) +{ + static struct MHD_Response *resp; + static struct GNUNET_TIME_Absolute a; + + (void) args; + if ( (GNUNET_TIME_absolute_is_past (a)) && + (NULL != resp) ) + { + MHD_destroy_response (resp); + resp = NULL; + } + if (NULL == resp) + { + struct GNUNET_TIME_Timestamp km; + char dat[128]; + + a = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_DAYS); + /* Round up to next full day to ensure the expiration + time does not become a fingerprint! */ + a = GNUNET_TIME_absolute_round_down (a, + GNUNET_TIME_UNIT_DAYS); + a = GNUNET_TIME_absolute_add (a, + GNUNET_TIME_UNIT_DAYS); + /* => /config response stays at most 48h in caches! */ + km = GNUNET_TIME_absolute_to_timestamp (a); + TALER_MHD_get_date_string (km.abs_time, + dat); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("supported_kyc_requirements", + TALER_KYCLOGIC_get_satisfiable ()), + GNUNET_JSON_pack_object_steal ( + "currency_specification", + TALER_CONFIG_currency_specs_to_json (TEH_cspec)), + GNUNET_JSON_pack_string ("currency", + TEH_currency), + GNUNET_JSON_pack_string ("name", + "taler-exchange"), + GNUNET_JSON_pack_string ("implementation", + "urn:net:taler:specs:taler-exchange:c-reference") + , + GNUNET_JSON_pack_string ("version", + EXCHANGE_PROTOCOL_VERSION)); + + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_EXPIRES, + dat)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CACHE_CONTROL, + "public,max-age=21600")); /* 6h */ + } + return MHD_queue_response (rc->connection, + MHD_HTTP_OK, + resp); +} + + +/* end of taler-exchange-httpd_config.c */ diff --git a/src/exchange/taler-exchange-httpd_config.h b/src/exchange/taler-exchange-httpd_config.h new file mode 100644 index 000000000..068f51d41 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_config.h @@ -0,0 +1,58 @@ +/* + This file is part of TALER + (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 EXCHANGEABILITY 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 taler-exchange-httpd_config.h + * @brief headers for /config handler + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_CONFIG_H +#define TALER_EXCHANGE_HTTPD_CONFIG_H +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Taler protocol version in the format CURRENT:REVISION:AGE + * as used by GNU libtool. See + * https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html + * + * Please be very careful when updating and follow + * https://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html#Updating-version-info + * precisely. Note that this version has NOTHING to do with the + * release version, and the format is NOT the same that semantic + * versioning uses either. + * + * When changing this version, you likely want to also update + * #TALER_PROTOCOL_CURRENT and #TALER_PROTOCOL_AGE in + * exchange_api_handle.c! + * + * Returned via both /config and /keys endpoints. + */ +#define EXCHANGE_PROTOCOL_VERSION "19:2:2" + + +/** + * Manages a /config call. + * + * @param rc context of the handler + * @param[in,out] args remaining arguments (ignored) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_config (struct TEH_RequestContext *rc, + const char *const args[]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_contract.c b/src/exchange/taler-exchange-httpd_contract.c new file mode 100644 index 000000000..defb7816d --- /dev/null +++ b/src/exchange/taler-exchange-httpd_contract.c @@ -0,0 +1,99 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_contract.c + * @brief Handle GET /contracts/$C_PUB requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_contract.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_responses.h" + + +MHD_RESULT +TEH_handler_contracts_get (struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct TALER_ContractDiffiePublicP contract_pub; + struct TALER_PurseContractPublicKeyP purse_pub; + void *econtract; + size_t econtract_size; + enum GNUNET_DB_QueryStatus qs; + struct TALER_PurseContractSignatureP econtract_sig; + MHD_RESULT res; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &contract_pub, + sizeof (contract_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB, + args[0]); + } + + qs = TEH_plugin->select_contract (TEH_plugin->cls, + &contract_pub, + &purse_pub, + &econtract_sig, + &econtract_size, + &econtract); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_contract"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_contract"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_CONTRACTS_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* handled below */ + } + res = TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("purse_pub", + &purse_pub), + GNUNET_JSON_pack_data_auto ("econtract_sig", + &econtract_sig), + GNUNET_JSON_pack_data_varsize ("econtract", + econtract, + econtract_size)); + GNUNET_free (econtract); + return res; +} + + +/* end of taler-exchange-httpd_contract.c */ diff --git a/src/exchange/taler-exchange-httpd_contract.h b/src/exchange/taler-exchange-httpd_contract.h new file mode 100644 index 000000000..dac6b81e3 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_contract.h @@ -0,0 +1,44 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_contract.h + * @brief Handle /coins/$COIN_PUB/contract requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_CONTRACT_H +#define TALER_EXCHANGE_HTTPD_CONTRACT_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/contracts/$C_PUB" request. Returns the + * encrypted contract. + * + * @param rc request context + * @param args array of additional options (length: 1, first is the contract_pub) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_contracts_get (struct TEH_RequestContext *rc, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_csr.c b/src/exchange/taler-exchange-httpd_csr.c new file mode 100644 index 000000000..e4fa4f5e4 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_csr.c @@ -0,0 +1,351 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General + Public License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_csr.c + * @brief Handle /csr requests + * @author Lucien Heuzeveldt + * @author Gian Demarmles + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_csr.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" + + +MHD_RESULT +TEH_handler_csr_melt (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + struct TALER_RefreshMasterSecretP rms; + unsigned int csr_requests_num; + const json_t *csr_requests; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("rms", + &rms), + GNUNET_JSON_spec_array_const ("nks", + &csr_requests), + GNUNET_JSON_spec_end () + }; + enum TALER_ErrorCode ec; + struct TEH_DenominationKey *dk; + + (void) args; + /* parse input */ + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + csr_requests_num = json_array_size (csr_requests); + if ( (TALER_MAX_FRESH_COINS <= csr_requests_num) || + (0 == csr_requests_num) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE, + NULL); + } + + { + struct GNUNET_CRYPTO_BlindingInputValues ewvs[csr_requests_num]; + { + struct GNUNET_CRYPTO_CsSessionNonce nonces[csr_requests_num]; + struct TALER_DenominationHashP denom_pub_hashes[csr_requests_num]; + struct TEH_CsDeriveData cdds[csr_requests_num]; + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[csr_requests_num]; + + for (unsigned int i = 0; i < csr_requests_num; i++) + { + uint32_t coin_off; + struct TALER_DenominationHashP *denom_pub_hash = &denom_pub_hashes[i]; + struct GNUNET_JSON_Specification csr_spec[] = { + GNUNET_JSON_spec_uint32 ("coin_offset", + &coin_off), + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + denom_pub_hash), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_array (rc->connection, + csr_requests, + csr_spec, + i, + -1); + if (GNUNET_OK != res) + { + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + TALER_cs_refresh_nonce_derive (&rms, + coin_off, + &nonces[i]); + } + + for (unsigned int i = 0; i < csr_requests_num; i++) + { + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = &nonces[i]; + const struct TALER_DenominationHashP *denom_pub_hash = + &denom_pub_hashes[i]; + + ewvs[i].cipher = GNUNET_CRYPTO_BSA_CS; + /* check denomination referenced by denom_pub_hash */ + { + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + dk = TEH_keys_denomination_by_hash_from_state (ksh, + denom_pub_hash, + NULL, + NULL); + if (NULL == dk) + { + return TEH_RESPONSE_reply_unknown_denom_pub_hash ( + rc->connection, + &denom_pub_hash[i]); + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) + { + /* This denomination is past the expiration time for withdraws/refreshes*/ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "csr-melt"); + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid, no need to check + for idempotency! */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "csr-melt"); + } + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "csr-melt"); + } + if (GNUNET_CRYPTO_BSA_CS != + dk->denom_pub.bsign_pub_key->cipher) + { + /* denomination is valid but not for CS */ + return TEH_RESPONSE_reply_invalid_denom_cipher_for_operation ( + rc->connection, + denom_pub_hash); + } + } + cdds[i].h_denom_pub = denom_pub_hash; + cdds[i].nonce = nonce; + } /* for (i) */ + ec = TEH_keys_denomination_cs_batch_r_pub (csr_requests_num, + cdds, + true, + r_pubs); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); + } + for (unsigned int i = 0; i < csr_requests_num; i++) + ewvs[i].details.cs_values = r_pubs[i]; + } /* end scope */ + + /* send response */ + { + json_t *csr_response_ewvs; + json_t *csr_response; + + csr_response_ewvs = json_array (); + for (unsigned int i = 0; i < csr_requests_num; i++) + { + json_t *csr_obj; + struct TALER_ExchangeWithdrawValues exw = { + .blinding_inputs = &ewvs[i] + }; + + csr_obj = GNUNET_JSON_PACK ( + TALER_JSON_pack_exchange_withdraw_values ("ewv", + &exw)); + GNUNET_assert (NULL != csr_obj); + GNUNET_assert (0 == + json_array_append_new (csr_response_ewvs, + csr_obj)); + } + csr_response = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("ewvs", + csr_response_ewvs)); + GNUNET_assert (NULL != csr_response); + return TALER_MHD_reply_json_steal (rc->connection, + csr_response, + MHD_HTTP_OK); + } + } +} + + +MHD_RESULT +TEH_handler_csr_withdraw (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + struct GNUNET_CRYPTO_CsSessionNonce nonce; + struct TALER_DenominationHashP denom_pub_hash; + struct GNUNET_CRYPTO_BlindingInputValues ewv = { + .cipher = GNUNET_CRYPTO_BSA_CS + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("nonce", + &nonce), + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &denom_pub_hash), + GNUNET_JSON_spec_end () + }; + struct TEH_DenominationKey *dk; + + (void) args; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + + { + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + dk = TEH_keys_denomination_by_hash_from_state (ksh, + &denom_pub_hash, + NULL, + NULL); + if (NULL == dk) + { + return TEH_RESPONSE_reply_unknown_denom_pub_hash ( + rc->connection, + &denom_pub_hash); + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) + { + /* This denomination is past the expiration time for withdraws/refreshes*/ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "csr-withdraw"); + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid, no need to check + for idempotency! */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "csr-withdraw"); + } + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "csr-withdraw"); + } + if (GNUNET_CRYPTO_BSA_CS != + dk->denom_pub.bsign_pub_key->cipher) + { + /* denomination is valid but not for CS */ + return TEH_RESPONSE_reply_invalid_denom_cipher_for_operation ( + rc->connection, + &denom_pub_hash); + } + } + + /* derive r_pub */ + { + enum TALER_ErrorCode ec; + const struct TEH_CsDeriveData cdd = { + .h_denom_pub = &denom_pub_hash, + .nonce = &nonce + }; + + ec = TEH_keys_denomination_cs_r_pub (&cdd, + false, + &ewv.details.cs_values); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); + } + } + { + struct TALER_ExchangeWithdrawValues exw = { + .blinding_inputs = &ewv + }; + + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + TALER_JSON_pack_exchange_withdraw_values ("ewv", + &exw)); + } +} + + +/* end of taler-exchange-httpd_csr.c */ diff --git a/src/exchange/taler-exchange-httpd_withdraw.h b/src/exchange/taler-exchange-httpd_csr.h index 8d2d8c182..615255f94 100644 --- a/src/exchange/taler-exchange-httpd_withdraw.h +++ b/src/exchange/taler-exchange-httpd_csr.h @@ -14,36 +14,43 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-exchange-httpd_withdraw.h - * @brief Handle /reserve/withdraw requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff + * @file taler-exchange-httpd_csr.h + * @brief Handle /csr-* requests + * @author Lucien Heuzeveldt + * @author Gian Demarmles */ -#ifndef TALER_EXCHANGE_HTTPD_WITHDRAW_H -#define TALER_EXCHANGE_HTTPD_WITHDRAW_H +#ifndef TALER_EXCHANGE_HTTPD_CSR_H +#define TALER_EXCHANGE_HTTPD_CSR_H #include <microhttpd.h> #include "taler-exchange-httpd.h" /** - * Handle a "/reserves/$RESERVE_PUB/withdraw" request. Parses the - * "reserve_pub" EdDSA key of the reserve and the requested "denom_pub" which - * specifies the key/value of the coin to be withdrawn, and checks that the - * signature "reserve_sig" makes this a valid withdrawal request from the - * specified reserve. If so, the envelope with the blinded coin "coin_ev" is - * passed down to execute the withdrawal operation. + * Handle a "/csr-melt" request. * * @param rc request context * @param root uploaded JSON data - * @param args array of additional options (first must be the - * reserve public key, the second one should be "withdraw") + * @param args empty array * @return MHD result code */ MHD_RESULT -TEH_handler_withdraw (struct TEH_RequestContext *rc, +TEH_handler_csr_melt (struct TEH_RequestContext *rc, const json_t *root, - const char *const args[2]); + const char *const args[]); + + +/** + * Handle a "/csr-withdraw" request. + * + * @param rc request context + * @param root uploaded JSON data + * @param args empty array + * @return MHD result code + */ +MHD_RESULT +TEH_handler_csr_withdraw (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); #endif diff --git a/src/exchange/taler-exchange-httpd_db.c b/src/exchange/taler-exchange-httpd_db.c index 3600d7931..6fec3fee4 100644 --- a/src/exchange/taler-exchange-httpd_db.c +++ b/src/exchange/taler-exchange-httpd_db.c @@ -19,9 +19,12 @@ * @author Christian Grothoff */ #include "platform.h" +#include <gnunet/gnunet_db_lib.h> #include <pthread.h> #include <jansson.h> #include <gnunet/gnunet_json_lib.h> +#include "taler_error_codes.h" +#include "taler_exchangedb_plugin.h" #include "taler_json_lib.h" #include "taler_mhd_lib.h" #include "taler_exchangedb_lib.h" @@ -29,19 +32,6 @@ #include "taler-exchange-httpd_responses.h" -/** - * How often should we retry a transaction before giving up - * (for transactions resulting in serialization/dead locks only). - * - * The current value is likely too high for production. We might want to - * benchmark good values once we have a good database setup. The code is - * expected to work correctly with any positive value, albeit inefficiently if - * we too aggressively force clients to retry the HTTP request merely because - * we have database serialization issues. - */ -#define MAX_TRANSACTION_COMMIT_RETRIES 100 - - enum GNUNET_DB_QueryStatus TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, struct MHD_Connection *connection, @@ -49,15 +39,15 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, MHD_RESULT *mhd_ret) { enum TALER_EXCHANGEDB_CoinKnownStatus cks; - struct TALER_DenominationHash h_denom_pub; - struct TALER_AgeHash age_hash; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_AgeCommitmentHash h_age_commitment = {{{0}}}; /* make sure coin is 'known' in database */ cks = TEH_plugin->ensure_coin_known (TEH_plugin->cls, coin, known_coin_id, &h_denom_pub, - &age_hash); + &h_age_commitment); switch (cks) { case TALER_EXCHANGEDB_CKS_ADDED: @@ -74,16 +64,50 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, NULL); return GNUNET_DB_STATUS_HARD_ERROR; case TALER_EXCHANGEDB_CKS_DENOM_CONFLICT: - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, - &coin->coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - case TALER_EXCHANGEDB_CKS_AGE_CONFLICT: - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + /* The exchange has a seen this coin before, but with a different denomination. + * Get the corresponding signature and sent it to the client as proof */ + { + struct + { + struct TALER_DenominationPublicKey pub; + struct TALER_DenominationSignature sig; + } prev_denom = {0}; + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TEH_plugin->get_signature_for_known_coin (TEH_plugin->cls, + &coin->coin_pub, + &prev_denom.pub, + &prev_denom.sig)) + { + /* There _should_ have been a result, because + * we ended here due to a conflict! */ + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + *mhd_ret = TEH_RESPONSE_reply_coin_denomination_conflict ( + connection, + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, + &coin->coin_pub, + &prev_denom.pub, + &prev_denom.sig); + + return GNUNET_DB_STATUS_HARD_ERROR; + } + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL: + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL: + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS: + *mhd_ret = TEH_RESPONSE_reply_coin_age_commitment_conflict ( connection, TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH, - &coin->coin_pub); + cks, + &h_denom_pub, + &coin->coin_pub, + &h_age_commitment); return GNUNET_DB_STATUS_HARD_ERROR; } GNUNET_assert (0); @@ -94,7 +118,7 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, enum GNUNET_GenericReturnValue TEH_DB_run_transaction (struct MHD_Connection *connection, const char *name, - enum TEH_MetricType mt, + enum TEH_MetricTypeRequest mt, MHD_RESULT *mhd_ret, TEH_DB_TransactionCallback cb, void *cb_cls) @@ -112,7 +136,7 @@ TEH_DB_run_transaction (struct MHD_Connection *connection, NULL); return GNUNET_SYSERR; } - GNUNET_assert (mt < TEH_MT_COUNT); + GNUNET_assert (mt < TEH_MT_REQUEST_COUNT); TEH_METRICS_num_requests[mt]++; for (unsigned int retries = 0; retries < MAX_TRANSACTION_COMMIT_RETRIES; @@ -136,10 +160,12 @@ TEH_DB_run_transaction (struct MHD_Connection *connection, connection, mhd_ret); if (0 > qs) + { TEH_plugin->rollback (TEH_plugin->cls); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - return GNUNET_SYSERR; - if (0 <= qs) + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + return GNUNET_SYSERR; + } + else { qs = TEH_plugin->commit (TEH_plugin->cls); if (GNUNET_DB_STATUS_HARD_ERROR == qs) @@ -162,6 +188,7 @@ TEH_DB_run_transaction (struct MHD_Connection *connection, return GNUNET_OK; TEH_METRICS_num_conflict[mt]++; } + TEH_plugin->rollback (TEH_plugin->cls); TALER_LOG_ERROR ("Transaction `%s' commit failed %u times\n", name, MAX_TRANSACTION_COMMIT_RETRIES); diff --git a/src/exchange/taler-exchange-httpd_db.h b/src/exchange/taler-exchange-httpd_db.h index 7c954ffe1..482bc5923 100644 --- a/src/exchange/taler-exchange-httpd_db.h +++ b/src/exchange/taler-exchange-httpd_db.h @@ -28,6 +28,19 @@ /** + * How often should we retry a transaction before giving up + * (for transactions resulting in serialization/dead locks only). + * + * The current value is likely too high for production. We might want to + * benchmark good values once we have a good database setup. The code is + * expected to work correctly with any positive value, albeit inefficiently if + * we too aggressively force clients to retry the HTTP request merely because + * we have database serialization issues. + */ +#define MAX_TRANSACTION_COMMIT_RETRIES 100 + + +/** * Ensure coin is known in the database, and handle conflicts and errors. * * @param coin the coin to make known @@ -83,7 +96,7 @@ typedef enum GNUNET_DB_QueryStatus enum GNUNET_GenericReturnValue TEH_DB_run_transaction (struct MHD_Connection *connection, const char *name, - enum TEH_MetricType mt, + enum TEH_MetricTypeRequest mt, MHD_RESULT *mhd_ret, TEH_DB_TransactionCallback cb, void *cb_cls); diff --git a/src/exchange/taler-exchange-httpd_deposit.c b/src/exchange/taler-exchange-httpd_deposit.c deleted file mode 100644 index 84741b5c3..000000000 --- a/src/exchange/taler-exchange-httpd_deposit.c +++ /dev/null @@ -1,474 +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 Affero 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_deposit.c - * @brief Handle /deposit requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler_json_lib.h" -#include "taler_mhd_lib.h" -#include "taler-exchange-httpd_deposit.h" -#include "taler-exchange-httpd_responses.h" -#include "taler_exchangedb_lib.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Send confirmation of deposit success to client. This function - * will create a signed message affirming the given information - * and return it to the client. By this, the exchange affirms that - * the coin had sufficient (residual) value for the specified - * transaction and that it will execute the requested deposit - * operation with the given wiring details. - * - * @param connection connection to the client - * @param coin_pub public key of the coin - * @param h_wire hash of wire details - * @param h_extensions hash of applicable extensions - * @param h_contract_terms hash of contract details - * @param exchange_timestamp exchange's timestamp - * @param refund_deadline until when this deposit be refunded - * @param merchant merchant public key - * @param amount_without_fee fraction of coin value to deposit, without the fee - * @return MHD result code - */ -static MHD_RESULT -reply_deposit_success (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_MerchantWireHash *h_wire, - const struct TALER_ExtensionContractHash *h_extensions, - const struct TALER_PrivateContractHash *h_contract_terms, - struct GNUNET_TIME_Timestamp exchange_timestamp, - struct GNUNET_TIME_Timestamp refund_deadline, - struct GNUNET_TIME_Timestamp wire_deadline, - const struct TALER_MerchantPublicKeyP *merchant, - const struct TALER_Amount *amount_without_fee) -{ - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP 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_timestamp_hton (exchange_timestamp), - .refund_deadline = GNUNET_TIME_timestamp_hton (refund_deadline), - .wire_deadline = GNUNET_TIME_timestamp_hton (wire_deadline), - .coin_pub = *coin_pub, - .merchant_pub = *merchant - }; - enum TALER_ErrorCode ec; - - if (NULL != h_extensions) - dc.h_extensions = *h_extensions; - TALER_amount_hton (&dc.amount_without_fee, - amount_without_fee); - if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign (&dc, - &pub, - &sig))) - { - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_timestamp ("exchange_timestamp", - exchange_timestamp), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); -} - - -/** - * Closure for #deposit_transaction. - */ -struct DepositContext -{ - /** - * Information about the deposit request. - */ - const struct TALER_EXCHANGEDB_Deposit *deposit; - - /** - * Our timestamp (when we received the request). - * Possibly updated by the transaction if the - * request is idempotent (was repeated). - */ - struct GNUNET_TIME_Timestamp exchange_timestamp; - - /** - * Hash of the payto URI. - */ - struct TALER_PaytoHash h_payto; - - /** - * Row of of the coin in the known_coins table. - */ - uint64_t known_coin_id; - -}; - - -/** - * Execute database transaction for /deposit. Runs the transaction - * logic; IF it returns a non-error code, the transaction logic MUST - * NOT queue a MHD response. IF it returns an hard error, the - * transaction logic MUST queue a MHD response and set @a mhd_ret. IF - * it returns the soft error code, the function MAY be called again to - * retry and MUST not queue a MHD response. - * - * @param cls a `struct DepositContext` - * @param connection MHD request context - * @param[out] mhd_ret set to MHD status on error - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -deposit_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct DepositContext *dc = cls; - enum GNUNET_DB_QueryStatus qs; - bool balance_ok; - bool in_conflict; - - qs = TEH_plugin->do_deposit (TEH_plugin->cls, - dc->deposit, - dc->known_coin_id, - &dc->h_payto, - false, /* FIXME-OEC: extension blocked */ - &dc->exchange_timestamp, - &balance_ok, - &in_conflict); - if (qs < 0) - { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - TALER_LOG_WARNING ("Failed to store /deposit information in database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "deposit"); - return qs; - } - if (in_conflict) - { - TEH_plugin->rollback (TEH_plugin->cls); - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT, - &dc->deposit->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - TEH_plugin->rollback (TEH_plugin->cls); - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &dc->deposit->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - return qs; -} - - -/** - * Handle a "/coins/$COIN_PUB/deposit" request. Parses the JSON, and, if - * successful, passes the JSON data to #deposit_transaction() to - * further check the details of the operation specified. If everything checks - * out, this will ultimately lead to the "/deposit" being executed, or - * rejected. - * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_deposit (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root) -{ - struct DepositContext dc; - struct TALER_EXCHANGEDB_Deposit deposit; - const char *payto_uri; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("merchant_payto_uri", - &payto_uri), - GNUNET_JSON_spec_fixed_auto ("wire_salt", - &deposit.wire_salt), - TALER_JSON_spec_amount ("contribution", - TEH_currency, - &deposit.amount_with_fee), - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &deposit.coin.denom_pub_hash), - TALER_JSON_spec_denom_sig ("ub_sig", - &deposit.coin.denom_sig), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &deposit.merchant_pub), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &deposit.h_contract_terms), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &deposit.csig), - GNUNET_JSON_spec_timestamp ("timestamp", - &deposit.timestamp), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("refund_deadline", - &deposit.refund_deadline)), - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &deposit.wire_deadline), - GNUNET_JSON_spec_end () - }; - struct TALER_MerchantWireHash h_wire; - - memset (&deposit, - 0, - sizeof (deposit)); - deposit.coin.coin_pub = *coin_pub; - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - { - GNUNET_break (0); - return MHD_NO; /* hard failure */ - } - if (GNUNET_NO == res) - { - GNUNET_break_op (0); - return MHD_YES; /* failure */ - } - } - /* validate merchant's wire details (as far as we can) */ - { - char *emsg; - - emsg = TALER_payto_validate (payto_uri); - if (NULL != emsg) - { - MHD_RESULT ret; - - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - emsg); - GNUNET_free (emsg); - return ret; - } - } - if (GNUNET_TIME_timestamp_cmp (deposit.refund_deadline, - >, - deposit.wire_deadline)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE, - NULL); - } - deposit.receiver_wire_account = (char *) payto_uri; - TALER_payto_hash (payto_uri, - &dc.h_payto); - TALER_merchant_wire_signature_hash (payto_uri, - &deposit.wire_salt, - &h_wire); - dc.deposit = &deposit; - - /* new deposit */ - dc.exchange_timestamp = GNUNET_TIME_timestamp_get (); - /* check denomination exists and is valid */ - { - struct TEH_DenominationKey *dk; - MHD_RESULT mret; - - dk = TEH_keys_denomination_by_hash (&deposit.coin.denom_pub_hash, - connection, - &mret); - if (NULL == dk) - { - GNUNET_JSON_parse_free (spec); - return mret; - } - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) - { - /* This denomination is past the expiration time for deposits */ - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &deposit.coin.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "DEPOSIT"); - } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) - { - /* This denomination is not yet valid */ - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &deposit.coin.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "DEPOSIT"); - } - if (dk->recoup_possible) - { - /* This denomination has been revoked */ - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &deposit.coin.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - "DEPOSIT"); - } - - deposit.deposit_fee = dk->meta.fee_deposit; - /* check coin signature */ - if (GNUNET_YES != - TALER_test_coin_valid (&deposit.coin, - &dk->denom_pub)) - { - TALER_LOG_WARNING ("Invalid coin passed for /deposit\n"); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_UNAUTHORIZED, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL); - } - } - if (0 < TALER_amount_cmp (&deposit.deposit_fee, - &deposit.amount_with_fee)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE, - NULL); - } - - if (GNUNET_OK != - TALER_wallet_deposit_verify (&deposit.amount_with_fee, - &deposit.deposit_fee, - &h_wire, - &deposit.h_contract_terms, - NULL /* h_extensions! */, - &deposit.coin.denom_pub_hash, - deposit.timestamp, - &deposit.merchant_pub, - deposit.refund_deadline, - &deposit.coin.coin_pub, - &deposit.csig)) - { - TALER_LOG_WARNING ("Invalid signature on /deposit request\n"); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_UNAUTHORIZED, - TALER_EC_EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID, - NULL); - } - - if (GNUNET_SYSERR == - TEH_plugin->preflight (TEH_plugin->cls)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - "preflight failure"); - } - - { - MHD_RESULT mhd_ret = MHD_NO; - enum GNUNET_DB_QueryStatus qs; - - /* make sure coin is 'known' in database */ - qs = TEH_make_coin_known (&deposit.coin, - connection, - &dc.known_coin_id, - &mhd_ret); - /* no transaction => no serialization failures should be possible */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - if (qs < 0) - return mhd_ret; - } - - - /* execute transaction */ - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "execute deposit", - TEH_MT_DEPOSIT, - &mhd_ret, - &deposit_transaction, - &dc)) - { - GNUNET_JSON_parse_free (spec); - return mhd_ret; - } - } - - /* generate regular response */ - { - struct TALER_Amount amount_without_fee; - MHD_RESULT res; - - GNUNET_assert (0 <= - TALER_amount_subtract (&amount_without_fee, - &deposit.amount_with_fee, - &deposit.deposit_fee)); - res = reply_deposit_success (connection, - &deposit.coin.coin_pub, - &h_wire, - NULL /* h_extensions! */, - &deposit.h_contract_terms, - dc.exchange_timestamp, - deposit.refund_deadline, - deposit.wire_deadline, - &deposit.merchant_pub, - &amount_without_fee); - GNUNET_JSON_parse_free (spec); - return res; - } -} - - -/* end of taler-exchange-httpd_deposit.c */ diff --git a/src/exchange/taler-exchange-httpd_deposits_get.c b/src/exchange/taler-exchange-httpd_deposits_get.c index 9a33f2b71..0850d19eb 100644 --- a/src/exchange/taler-exchange-httpd_deposits_get.c +++ b/src/exchange/taler-exchange-httpd_deposits_get.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2017, 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 Affero General Public License as published by the Free Software @@ -23,6 +23,7 @@ #include <jansson.h> #include <microhttpd.h> #include <pthread.h> +#include "taler_dbevents.h" #include "taler_json_lib.h" #include "taler_mhd_lib.h" #include "taler_signatures.h" @@ -32,83 +33,52 @@ /** - * A merchant asked for details about a deposit. Provide - * them. Generates the 200 reply. - * - * @param connection connection to the client - * @param h_contract_terms hash of the contract - * @param h_wire hash of wire account details - * @param coin_pub public key of the coin - * @param coin_contribution how much did the coin we asked about - * contribute to the total transfer value? (deposit value minus fee) - * @param wtid raw wire transfer identifier - * @param exec_time execution time of the wire transfer - * @return MHD result code + * Closure for #handle_wtid_data. */ -static MHD_RESULT -reply_deposit_details (struct MHD_Connection *connection, - const struct TALER_PrivateContractHash *h_contract_terms, - const struct TALER_MerchantWireHash *h_wire, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *coin_contribution, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct GNUNET_TIME_Timestamp exec_time) +struct DepositWtidContext { - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - struct TALER_ConfirmWirePS cw = { - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE), - .purpose.size = htonl (sizeof (cw)), - .h_wire = *h_wire, - .h_contract_terms = *h_contract_terms, - .wtid = *wtid, - .coin_pub = *coin_pub, - .execution_time = GNUNET_TIME_timestamp_hton (exec_time) - }; - enum TALER_ErrorCode ec; - TALER_amount_hton (&cw.coin_contribution, - coin_contribution); - if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign (&cw, - &pub, - &sig))) - { - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("wtid", - wtid), - GNUNET_JSON_pack_timestamp ("execution_time", - exec_time), - TALER_JSON_pack_amount ("coin_contribution", - coin_contribution), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); -} + /** + * Kept in a DLL. + */ + struct DepositWtidContext *next; + + /** + * Kept in a DLL. + */ + struct DepositWtidContext *prev; + /** + * Context for the request we are processing. + */ + struct TEH_RequestContext *rc; -/** - * Closure for #handle_wtid_data. - */ -struct DepositWtidContext -{ + /** + * Subscription for the database event we are waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Hash over the proposal data of the contract for which this deposit is made. + */ + struct TALER_PrivateContractHashP h_contract_terms; /** - * Deposit details. + * Hash over the wiring information of the merchant. */ - const struct TALER_DepositTrackPS *tps; + struct TALER_MerchantWireHashP h_wire; /** - * Public key of the merchant. + * The Merchant's public key. The deposit inquiry request is to be + * signed by the corresponding private key (using EdDSA). */ - const struct TALER_MerchantPublicKeyP *merchant_pub; + struct TALER_MerchantPublicKeyP merchant; + + /** + * The coin's public key. This is the value that must have been + * signed (blindly) by the Exchange. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; /** * Set by #handle_wtid data to the wire transfer ID. @@ -116,6 +86,12 @@ struct DepositWtidContext struct TALER_WireTransferIdentifierRawP wtid; /** + * Signature by the merchant. + */ + struct TALER_MerchantSignatureP merchant_sig; + + + /** * Set by #handle_wtid data to the coin's contribution to the wire transfer. */ struct TALER_Amount coin_contribution; @@ -131,6 +107,11 @@ struct DepositWtidContext struct GNUNET_TIME_Timestamp execution_time; /** + * Timeout of the request, for long-polling. + */ + struct GNUNET_TIME_Absolute timeout; + + /** * Set by #handle_wtid to the coin contribution to the transaction * (that is, @e coin_contribution minus @e coin_fee). */ @@ -142,15 +123,107 @@ struct DepositWtidContext struct TALER_EXCHANGEDB_KycStatus kyc; /** + * AML status information for the receiving account. + */ + enum TALER_AmlDecisionState aml_decision; + + /** * Set to #GNUNET_YES by #handle_wtid if the wire transfer is still pending * (and the above were not set). * Set to #GNUNET_SYSERR if there was a serious error. */ enum GNUNET_GenericReturnValue pending; + + /** + * #GNUNET_YES if we were suspended, #GNUNET_SYSERR + * if we were woken up due to shutdown. + */ + enum GNUNET_GenericReturnValue suspended; }; /** + * Head of DLL of suspended requests. + */ +static struct DepositWtidContext *dwc_head; + +/** + * Tail of DLL of suspended requests. + */ +static struct DepositWtidContext *dwc_tail; + + +void +TEH_deposits_get_cleanup () +{ + struct DepositWtidContext *n; + + for (struct DepositWtidContext *ctx = dwc_head; + NULL != ctx; + ctx = n) + { + n = ctx->next; + GNUNET_assert (GNUNET_YES == ctx->suspended); + ctx->suspended = GNUNET_SYSERR; + MHD_resume_connection (ctx->rc->connection); + GNUNET_CONTAINER_DLL_remove (dwc_head, + dwc_tail, + ctx); + } +} + + +/** + * A merchant asked for details about a deposit. Provide + * them. Generates the 200 reply. + * + * @param connection connection to the client + * @param ctx details to respond with + * @return MHD result code + */ +static MHD_RESULT +reply_deposit_details ( + struct MHD_Connection *connection, + const struct DepositWtidContext *ctx) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_confirm_wire_sign ( + &TEH_keys_exchange_sign_, + &ctx->h_wire, + &ctx->h_contract_terms, + &ctx->wtid, + &ctx->coin_pub, + ctx->execution_time, + &ctx->coin_delta, + &pub, + &sig))) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("wtid", + &ctx->wtid), + GNUNET_JSON_pack_timestamp ("execution_time", + ctx->execution_time), + TALER_JSON_pack_amount ("coin_contribution", + &ctx->coin_delta), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** * Execute a "deposits" GET. Returns the transfer information * associated with the given deposit. * @@ -177,17 +250,17 @@ deposits_get_transaction (void *cls, struct TALER_Amount fee; qs = TEH_plugin->lookup_transfer_by_deposit (TEH_plugin->cls, - &ctx->tps->h_contract_terms, - &ctx->tps->h_wire, - &ctx->tps->coin_pub, - ctx->merchant_pub, - + &ctx->h_contract_terms, + &ctx->h_wire, + &ctx->coin_pub, + &ctx->merchant, &pending, &ctx->wtid, &ctx->execution_time, &ctx->coin_contribution, &fee, - &ctx->kyc); + &ctx->kyc, + &ctx->aml_decision); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) @@ -224,55 +297,140 @@ deposits_get_transaction (void *cls, /** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct DepositWtidContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct DepositWtidContext *ctx = cls; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + if (GNUNET_YES != ctx->suspended) + return; /* might get multiple wake-up events */ + GNUNET_CONTAINER_DLL_remove (dwc_head, + dwc_tail, + ctx); + GNUNET_async_scope_enter (&ctx->rc->async_scope_id, + &old_scope); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming request handling\n"); + TEH_check_invariants (); + ctx->suspended = GNUNET_NO; + MHD_resume_connection (ctx->rc->connection); + TALER_MHD_daemon_trigger (); + TEH_check_invariants (); + GNUNET_async_scope_restore (&old_scope); +} + + +/** * Lookup and return the wire transfer identifier. * - * @param connection the MHD connection to handle - * @param tps signed request to execute - * @param merchant_pub public key from the merchant + * @param ctx context of the signed request to execute * @return MHD result code */ static MHD_RESULT handle_track_transaction_request ( - struct MHD_Connection *connection, - const struct TALER_DepositTrackPS *tps, - const struct TALER_MerchantPublicKeyP *merchant_pub) + struct DepositWtidContext *ctx) { - MHD_RESULT mhd_ret; - struct DepositWtidContext ctx = { - .tps = tps, - .merchant_pub = merchant_pub - }; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "handle deposits GET", - TEH_MT_OTHER, - &mhd_ret, - &deposits_get_transaction, - &ctx)) - return mhd_ret; - if (GNUNET_SYSERR == ctx.pending) + struct MHD_Connection *connection = ctx->rc->connection; + + if ( (GNUNET_TIME_absolute_is_future (ctx->timeout)) && + (NULL == ctx->eh) ) + { + struct TALER_CoinDepositEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_DEPOSIT_STATUS_CHANGED), + .merchant_pub = ctx->merchant + }; + + ctx->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (ctx->timeout), + &rep.header, + &db_event_cb, + ctx); + GNUNET_break (NULL != ctx->eh); + } + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "handle deposits GET", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &deposits_get_transaction, + ctx)) + return mhd_ret; + } + if (GNUNET_SYSERR == ctx->pending) return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_INVARIANT_FAILURE, "wire fees exceed aggregate in database"); - if (GNUNET_YES == ctx.pending) + if (GNUNET_YES == ctx->pending) + { + if ( (GNUNET_TIME_absolute_is_future (ctx->timeout)) && + (GNUNET_NO == ctx->suspended) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending request handling\n"); + GNUNET_CONTAINER_DLL_insert (dwc_head, + dwc_tail, + ctx); + ctx->suspended = GNUNET_YES; + MHD_suspend_connection (connection); + return MHD_YES; + } return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - ctx.kyc.payment_target_uuid), + GNUNET_JSON_pack_allow_null ( + (0 == ctx->kyc.requirement_row) + ? GNUNET_JSON_pack_string ("requirement_row", + NULL) + : GNUNET_JSON_pack_uint64 ("requirement_row", + ctx->kyc.requirement_row)), + GNUNET_JSON_pack_uint64 ("aml_decision", + (uint32_t) ctx->aml_decision), GNUNET_JSON_pack_bool ("kyc_ok", - ctx.kyc.ok), + ctx->kyc.ok), GNUNET_JSON_pack_timestamp ("execution_time", - ctx.execution_time)); + ctx->execution_time)); + } return reply_deposit_details (connection, - &tps->h_contract_terms, - &tps->h_wire, - &tps->coin_pub, - &ctx.coin_delta, - &ctx.wtid, - ctx.execution_time); + ctx); +} + + +/** + * Function called to clean up a context. + * + * @param rc request context with data to clean up + */ +static void +dwc_cleaner (struct TEH_RequestContext *rc) +{ + struct DepositWtidContext *ctx = rc->rh_ctx; + + GNUNET_assert (GNUNET_NO == ctx->suspended); + if (NULL != ctx->eh) + { + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + ctx->eh); + ctx->eh = NULL; + } + GNUNET_free (ctx); } @@ -280,85 +438,87 @@ MHD_RESULT TEH_handler_deposits_get (struct TEH_RequestContext *rc, const char *const args[4]) { - enum GNUNET_GenericReturnValue res; - struct TALER_MerchantSignatureP merchant_sig; - struct TALER_DepositTrackPS tps = { - .purpose.size = htonl (sizeof (tps)), - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_TRACK_TRANSACTION) - }; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &tps.h_wire, - sizeof (tps.h_wire))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE, - args[0]); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[1], - strlen (args[1]), - &tps.merchant, - sizeof (tps.merchant))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB, - args[1]); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[2], - strlen (args[2]), - &tps.h_contract_terms, - sizeof (tps.h_contract_terms))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS, - args[2]); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[3], - strlen (args[3]), - &tps.coin_pub, - sizeof (tps.coin_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB, - args[3]); - } - res = TALER_MHD_parse_request_arg_data (rc->connection, - "merchant_sig", - &merchant_sig, - sizeof (merchant_sig)); - if (GNUNET_SYSERR == res) - return MHD_NO; /* internal error */ - if (GNUNET_NO == res) - return MHD_YES; /* parse error */ - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_TRACK_TRANSACTION, - &tps, - &merchant_sig.eddsa_sig, - &tps.merchant.eddsa_pub)) + struct DepositWtidContext *ctx = rc->rh_ctx; + + if (NULL == ctx) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID, - NULL); + ctx = GNUNET_new (struct DepositWtidContext); + ctx->rc = rc; + rc->rh_ctx = ctx; + rc->rh_cleaner = &dwc_cleaner; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &ctx->h_wire, + sizeof (ctx->h_wire))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE, + args[0]); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[1], + strlen (args[1]), + &ctx->merchant, + sizeof (ctx->merchant))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB, + args[1]); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[2], + strlen (args[2]), + &ctx->h_contract_terms, + sizeof (ctx->h_contract_terms))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS, + args[2]); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[3], + strlen (args[3]), + &ctx->coin_pub, + sizeof (ctx->coin_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB, + args[3]); + } + TALER_MHD_parse_request_arg_auto_t (rc->connection, + "merchant_sig", + &ctx->merchant_sig); + TALER_MHD_parse_request_timeout (rc->connection, + &ctx->timeout); + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + { + if (GNUNET_OK != + TALER_merchant_deposit_verify (&ctx->merchant, + &ctx->coin_pub, + &ctx->h_contract_terms, + &ctx->h_wire, + &ctx->merchant_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID, + NULL); + } + } } - return handle_track_transaction_request (rc->connection, - &tps, - &tps.merchant); + return handle_track_transaction_request (ctx); } diff --git a/src/exchange/taler-exchange-httpd_deposits_get.h b/src/exchange/taler-exchange-httpd_deposits_get.h index aee7521a5..c7b1698bb 100644 --- a/src/exchange/taler-exchange-httpd_deposits_get.h +++ b/src/exchange/taler-exchange-httpd_deposits_get.h @@ -27,6 +27,13 @@ /** + * Resume long pollers on GET /deposits. + */ +void +TEH_deposits_get_cleanup (void); + + +/** * Handle a "/deposits/$H_WIRE/$MERCHANT_PUB/$H_CONTRACT_TERMS/$COIN_PUB" * request. * diff --git a/src/exchange/taler-exchange-httpd_extensions.c b/src/exchange/taler-exchange-httpd_extensions.c index 8723bebc8..d62a618ae 100644 --- a/src/exchange/taler-exchange-httpd_extensions.c +++ b/src/exchange/taler-exchange-httpd_extensions.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021 Taler Systems SA + 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 Affero General Public License as published by the Free Software @@ -14,119 +14,22 @@ */ /** * @file taler-exchange-httpd_extensions.c - * @brief Handle extensions (age-restriction, peer2peer) + * @brief Handle extensions (age-restriction, policy extensions) * @author Özgür Kesim */ #include "platform.h" #include <gnunet/gnunet_json_lib.h> #include "taler_dbevents.h" +#include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_extensions.h" +#include "taler_extensions_policy.h" #include "taler_json_lib.h" #include "taler_mhd_lib.h" #include "taler_extensions.h" #include <jansson.h> /** - * @brief implements the TALER_Extension.parse_and_set_config interface. - */ -static enum GNUNET_GenericReturnValue -age_restriction_parse_and_set_config (struct TALER_Extension *this, - const json_t *config) -{ - enum GNUNET_GenericReturnValue ret; - struct TALER_AgeMask mask = {0}; - - ret = TALER_agemask_parse_json (config, &mask); - if (GNUNET_OK != ret) - return ret; - - if (this != NULL && TALER_Extension_AgeRestriction == this->type) - { - if (NULL != this->config) - { - GNUNET_free (this->config); - } - this->config = GNUNET_malloc (sizeof(struct TALER_AgeMask)); - GNUNET_memcpy (this->config, &mask, sizeof(struct TALER_AgeMask)); - } - - return GNUNET_OK; -} - - -/** - * @brief implements the TALER_Extension.test_config interface. - */ -static enum GNUNET_GenericReturnValue -age_restriction_test_config (const json_t *config) -{ - return age_restriction_parse_and_set_config (NULL, config); -} - - -/** - * @brief implements the TALER_Extension.config_to_json interface. - */ -static json_t * -age_restriction_config_to_json (const struct TALER_Extension *this) -{ - const struct TALER_AgeMask *mask; - if (NULL == this || TALER_Extension_AgeRestriction != this->type) - return NULL; - - mask = (struct TALER_AgeMask *) this->config; - json_t *config = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("extension", this->name), - GNUNET_JSON_pack_string ("mask", - TALER_age_mask_to_string (mask)) - ); - - return config; -} - - -/* The extension for age restriction */ -static struct TALER_Extension extension_age_restriction = { - .type = TALER_Extension_AgeRestriction, - .name = "age_restriction", - .critical = false, - .config = NULL, // disabled per default - .test_config = &age_restriction_test_config, - .parse_and_set_config = &age_restriction_parse_and_set_config, - .config_to_json = &age_restriction_config_to_json, -}; - -/* TODO: The extension for peer2peer */ -static struct TALER_Extension extension_peer2peer = { - .type = TALER_Extension_Peer2Peer, - .name = "peer2peer", - .critical = false, - .config = NULL, // disabled per default - .test_config = NULL, // TODO - .parse_and_set_config = NULL, // TODO - .config_to_json = NULL, // TODO -}; - - -/** - * Create a list with the extensions for Age Restriction and Peer2Peer - */ -static struct TALER_Extension ** -get_known_extensions () -{ - - struct TALER_Extension **list = GNUNET_new_array (TALER_Extension_Max + 1, - struct TALER_Extension *); - list[TALER_Extension_AgeRestriction] = &extension_age_restriction; - list[TALER_Extension_Peer2Peer] = &extension_peer2peer; - list[TALER_Extension_Max] = NULL; - - return list; -} - - -/** * Handler listening for extensions updates by other exchange * services. */ @@ -137,8 +40,8 @@ static struct GNUNET_DB_EventHandler *extensions_eh; * the extensions data in the database. * * @param cls NULL - * @param extra unused - * @param extra_size number of bytes in @a extra unused + * @param extra type of the extension + * @param extra_size number of bytes in @a extra */ static void extension_update_event_cb (void *cls, @@ -146,12 +49,14 @@ extension_update_event_cb (void *cls, size_t extra_size) { (void) cls; + uint32_t nbo_type; enum TALER_Extension_Type type; + const struct TALER_Extension *extension; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received extensions update event\n"); - if (sizeof(enum TALER_Extension_Type) != extra_size) + if (sizeof(nbo_type) != extra_size) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, @@ -159,87 +64,167 @@ extension_update_event_cb (void *cls, return; } - type = *(enum TALER_Extension_Type *) extra; - if (type <0 || type >= TALER_Extension_Max) + GNUNET_assert (NULL != extra); + + nbo_type = *(uint32_t *) extra; + type = (enum TALER_Extension_Type) ntohl (nbo_type); + + /* Get the corresponding extension */ + extension = TALER_extensions_get_by_type (type); + if (NULL == extension) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Oops, incorrect type for TALER_Extension_type\n"); + "Oops, unknown extension type: %d\n", type); return; } - // Get the config from the database as string + // Get the manifest from the database as string { - char *config_str; + char *manifest_str = NULL; enum GNUNET_DB_QueryStatus qs; - struct TALER_Extension *extension; json_error_t err; - json_t *config; + json_t *manifest_js; enum GNUNET_GenericReturnValue ret; - // TODO: make this a safe lookup - extension = TEH_extensions[type]; - - qs = TEH_plugin->get_extension_config (TEH_plugin->cls, - extension->name, - &config_str); + qs = TEH_plugin->get_extension_manifest (TEH_plugin->cls, + extension->name, + &manifest_str); if (qs < 0) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Couldn't get extension config\n"); + "Couldn't get extension manifest\n"); GNUNET_break (0); return; } + // No config found -> disable extension + if (NULL == manifest_str) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No manifest found for extension %s, disabling it\n", + extension->name); + extension->disable ((struct TALER_Extension *) extension); + return; + } + // Parse the string as JSON - config = json_loads (config_str, JSON_DECODE_ANY, &err); - if (NULL == config) + manifest_js = json_loads (manifest_str, JSON_DECODE_ANY, &err); + if (NULL == manifest_js) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse config for extension `%s' as JSON: %s (%s)\n", + "Failed to parse manifest for extension `%s' as JSON: %s (%s)\n", extension->name, err.text, err.source); GNUNET_break (0); + free (manifest_str); return; } // Call the parser for the extension - ret = extension->parse_and_set_config (extension, config); + ret = extension->load_config ( + json_object_get (manifest_js, "config"), + (struct TALER_Extension *) extension); + if (GNUNET_OK != ret) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Couldn't parse configuration for extension %s from the database", - extension->name); + "Couldn't parse configuration for extension %s from the manifest in the database: %s\n", + extension->name, + manifest_str); GNUNET_break (0); } + + free (manifest_str); + json_decref (manifest_js); + } + + /* Special case age restriction: Update global flag and mask */ + if (TALER_Extension_AgeRestriction == type) + { + const struct TALER_AgeRestrictionConfig *conf = + TALER_extensions_get_age_restriction_config (); + TEH_age_restriction_enabled = false; + if (NULL != conf) + { + TEH_age_restriction_enabled = extension->enabled; + TEH_age_restriction_config = *conf; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "[age restriction] DB event has changed the config to %s with mask: %s\n", + TEH_age_restriction_enabled ? "enabled": "DISABLED", + TALER_age_mask_to_string (&conf->mask)); + } } + + // Finally, call TEH_keys_update_states in order to refresh the cached + // values. + TEH_keys_update_states (); } enum GNUNET_GenericReturnValue TEH_extensions_init () { - TEH_extensions = get_known_extensions (); + /* Set the event handler for updates */ + struct GNUNET_DB_EventHeaderP ev = { + .size = htons (sizeof (ev)), + .type = htons (TALER_DBEVENT_EXCHANGE_EXTENSIONS_UPDATED), + }; + + /* Load the shared libraries first */ + if (GNUNET_OK != + TALER_extensions_init (TEH_cfg)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "failed to load extensions"); + return GNUNET_SYSERR; + } + /* Check for age restriction */ { - struct GNUNET_DB_EventHeaderP ev = { - .size = htons (sizeof (ev)), - .type = htons (TALER_DBEVENT_EXCHANGE_EXTENSIONS_UPDATED), - }; + const struct TALER_AgeRestrictionConfig *arc; - extensions_eh = TEH_plugin->event_listen (TEH_plugin->cls, - GNUNET_TIME_UNIT_FOREVER_REL, - &ev, - &extension_update_event_cb, - NULL); - if (NULL == extensions_eh) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } + if (NULL != + (arc = TALER_extensions_get_age_restriction_config ())) + TEH_age_restriction_config = *arc; + } + + extensions_eh = TEH_plugin->event_listen (TEH_plugin->cls, + GNUNET_TIME_UNIT_FOREVER_REL, + &ev, + &extension_update_event_cb, + NULL); + if (NULL == extensions_eh) + { + GNUNET_break (0); + return GNUNET_SYSERR; } + + /* Trigger the initial load of configuration from the db */ + for (const struct TALER_Extensions *it = TALER_extensions_get_head (); + NULL != it && NULL != it->extension; + it = it->next) + { + const struct TALER_Extension *ext = it->extension; + uint32_t typ = htonl (ext->type); + json_t *jmani; + char *manifest; + + jmani = ext->manifest (ext); + manifest = json_dumps (jmani, + JSON_COMPACT); + json_decref (jmani); + TEH_plugin->set_extension_manifest (TEH_plugin->cls, + ext->name, + manifest); + free (manifest); + extension_update_event_cb (NULL, + &typ, + sizeof(typ)); + } + return GNUNET_OK; } @@ -256,4 +241,202 @@ TEH_extensions_done () } +/* + * @brief Execute database transactions for /extensions/policy_* POST requests. + * + * @param cls a `struct TALER_PolicyFulfillmentOutcome` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +policy_fulfillment_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct TALER_PolicyFulfillmentTransactionData *fulfillment = cls; + + /* FIXME[oec]: use connection and mhd_ret? */ + (void) connection; + (void) mhd_ret; + + return TEH_plugin->add_policy_fulfillment_proof (TEH_plugin->cls, + fulfillment); +} + + +/* FIXME[oec]-#7999: In this handler: do we transition correctly between states? */ +MHD_RESULT +TEH_extensions_post_handler ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + const struct TALER_Extension *ext = NULL; + json_t *output; + struct TALER_PolicyDetails *policy_details = NULL; + size_t policy_details_count = 0; + + + if (NULL == args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_OPERATION_UNKNOWN, + "/extensions/$EXTENSION"); + } + + ext = TALER_extensions_get_by_name (args[0]); + if (NULL == ext) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_OPERATION_UNKNOWN, + "/extensions/$EXTENSION unknown"); + } + + if (NULL == ext->policy_post_handler) + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_EXCHANGE_GENERIC_OPERATION_UNKNOWN, + "POST /extensions/$EXTENSION not supported"); + + /* Extract hash_codes and retrieve related policy_details from the DB */ + { + enum GNUNET_GenericReturnValue ret; + enum GNUNET_DB_QueryStatus qs; + const char *error_msg; + struct GNUNET_HashCode *hcs; + size_t len; + json_t*val; + size_t idx; + json_t *jhash_codes = json_object_get (root, + "policy_hash_codes"); + if (! json_is_array (jhash_codes)) + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_OPERATION_UNKNOWN, + "policy_hash_codes are missing"); + + len = json_array_size (jhash_codes); + hcs = GNUNET_new_array (len, + struct GNUNET_HashCode); + policy_details = GNUNET_new_array (len, + struct TALER_PolicyDetails); + + json_array_foreach (jhash_codes, idx, val) + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, &hcs[idx]), + GNUNET_JSON_spec_end () + }; + + ret = GNUNET_JSON_parse (val, + spec, + &error_msg, + NULL); + if (GNUNET_OK != ret) + break; + + qs = TEH_plugin->get_policy_details (TEH_plugin->cls, + &hcs[idx], + &policy_details[idx]); + if (0 > qs) + { + GNUNET_free (hcs); + GNUNET_free (policy_details); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_OPERATION_UNKNOWN, + "a policy_hash_code couldn't be found"); + } + + /* We proceed according to the state of fulfillment */ + switch (policy_details[idx].fulfillment_state) + { + case TALER_PolicyFulfillmentReady: + break; + case TALER_PolicyFulfillmentInsufficient: + error_msg = "a policy is not yet fully funded"; + ret = GNUNET_SYSERR; + break; + case TALER_PolicyFulfillmentTimeout: + error_msg = "a policy is has already timed out"; + ret = GNUNET_SYSERR; + break; + case TALER_PolicyFulfillmentSuccess: + /* FIXME[oec]-#8001: Idempotency handling. */ + GNUNET_break (0); + break; + case TALER_PolicyFulfillmentFailure: + /* FIXME[oec]-#7999: What to do in the failure case? */ + GNUNET_break (0); + break; + default: + /* Unknown state */ + GNUNET_assert (0); + } + + if (GNUNET_OK != ret) + break; + } + + GNUNET_free (hcs); + + if (GNUNET_OK != ret) + { + GNUNET_free (policy_details); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_OPERATION_UNKNOWN, + error_msg); + } + } + + + if (GNUNET_OK != + ext->policy_post_handler (root, + &args[1], + policy_details, + policy_details_count, + &output)) + { + return TALER_MHD_reply_json_steal ( + rc->connection, + output, + MHD_HTTP_BAD_REQUEST); + } + + /* execute fulfillment transaction */ + { + MHD_RESULT mhd_ret; + struct TALER_PolicyFulfillmentTransactionData fulfillment = { + .proof = root, + .timestamp = GNUNET_TIME_timestamp_get (), + .details = policy_details, + .details_count = policy_details_count + }; + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "execute policy fulfillment", + TEH_MT_REQUEST_POLICY_FULFILLMENT, + &mhd_ret, + &policy_fulfillment_transaction, + &fulfillment)) + { + json_decref (output); + return mhd_ret; + } + } + + return TALER_MHD_reply_json_steal (rc->connection, + output, + MHD_HTTP_OK); +} + + /* end of taler-exchange-httpd_extensions.c */ diff --git a/src/exchange/taler-exchange-httpd_extensions.h b/src/exchange/taler-exchange-httpd_extensions.h index 4659b653e..e435f8f03 100644 --- a/src/exchange/taler-exchange-httpd_extensions.h +++ b/src/exchange/taler-exchange-httpd_extensions.h @@ -40,4 +40,19 @@ TEH_extensions_init (void); void TEH_extensions_done (void); + +/** + * Handle POST "/extensions/..." requests. + * + * @param rc request context + * @param root uploaded JSON data + * @param args array of additional options + * @return MHD result code + */ +MHD_RESULT +TEH_extensions_post_handler ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); + #endif diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c index 30bbe8ebb..0ec28e950 100644 --- a/src/exchange/taler-exchange-httpd_keys.c +++ b/src/exchange/taler-exchange-httpd_keys.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020, 2021 Taler Systems SA + 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 Affero General Public License as published by the Free Software @@ -17,12 +17,15 @@ * @file taler-exchange-httpd_keys.c * @brief management of our various keys * @author Christian Grothoff + * @author Özgür Kesim */ #include "platform.h" #include "taler_json_lib.h" #include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_dbevents.h" #include "taler-exchange-httpd.h" +#include "taler-exchange-httpd_config.h" #include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_responses.h" #include "taler_exchangedb_plugin.h" @@ -43,24 +46,6 @@ /** - * Taler protocol version in the format CURRENT:REVISION:AGE - * as used by GNU libtool. See - * https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html - * - * Please be very careful when updating and follow - * https://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html#Updating-version-info - * precisely. Note that this version has NOTHING to do with the - * release version, and the format is NOT the same that semantic - * versioning uses either. - * - * When changing this version, you likely want to also update - * #TALER_PROTOCOL_CURRENT and #TALER_PROTOCOL_AGE in - * exchange_api_handle.c! - */ -#define EXCHANGE_PROTOCOL_VERSION "12:0:0" - - -/** * Information about a denomination on offer by the denomination helper. */ struct HelperDenomination @@ -80,7 +65,7 @@ struct HelperDenomination /** * Hash of the full denomination key. */ - struct TALER_DenominationHash h_denom_pub; + struct TALER_DenominationHashP h_denom_pub; /** * Signature over this key from the security module's key. @@ -103,6 +88,11 @@ struct HelperDenomination */ struct TALER_RsaPubHashP h_rsa; + /** + * Hash of the CS key. + */ + struct TALER_CsPubHashP h_cs; + } h_details; /** @@ -172,10 +162,9 @@ struct HelperSignkey /** - * State associated with the crypto helpers / security modules. - * Created per-thread, but NOT updated when the #key_generation - * is updated (instead constantly kept in sync whenever - * #TEH_keys_get_state() is called). + * State associated with the crypto helpers / security modules. NOT updated + * when the #key_generation is updated (instead constantly kept in sync + * whenever #TEH_keys_get_state() is called). */ struct HelperState { @@ -188,7 +177,12 @@ struct HelperState /** * Handle for the denom/RSA helper. */ - struct TALER_CRYPTO_RsaDenominationHelper *dh; + struct TALER_CRYPTO_RsaDenominationHelper *rsadh; + + /** + * Handle for the denom/CS helper. + */ + struct TALER_CRYPTO_CsDenominationHelper *csdh; /** * Map from H(denom_pub) to `struct HelperDenomination` entries. @@ -201,6 +195,11 @@ struct HelperState struct GNUNET_CONTAINER_MultiHashMap *rsa_keys; /** + * Map from H(cs_pub) to `struct HelperDenomination` entries. + */ + struct GNUNET_CONTAINER_MultiHashMap *cs_keys; + + /** * Map from `struct TALER_ExchangePublicKey` to `struct HelperSignkey` * entries. Based on the fact that a `struct GNUNET_PeerIdentity` is also * an EdDSA public key. @@ -229,6 +228,11 @@ struct KeysResponseData struct MHD_Response *response_uncompressed; /** + * ETag for these responses. + */ + char *etag; + + /** * Cherry-picking timestamp the client must have set for this * response to be valid. 0 if this is the "full" response. * The client's request must include this date or a higher one @@ -264,7 +268,6 @@ struct SigningKey }; - struct TEH_KeyStateHandle { @@ -282,12 +285,28 @@ struct TEH_KeyStateHandle struct GNUNET_CONTAINER_MultiPeerMap *signkey_map; /** + * Head of DLL of our global fees. + */ + struct TEH_GlobalFee *gf_head; + + /** + * Tail of DLL of our global fees. + */ + struct TEH_GlobalFee *gf_tail; + + /** * json array with the auditors of this exchange. Contains exactly * the information needed for the "auditors" field of the /keys response. */ json_t *auditors; /** + * json array with the global fees of this exchange. Contains exactly + * the information needed for the "global_fees" field of the /keys response. + */ + json_t *global_fees; + + /** * Sorted array of responses to /keys (MUST be sorted by cherry-picking date) of * length @e krd_array_length; */ @@ -371,13 +390,119 @@ struct SuspendedKeysRequests /** + * Information we track about wire fees. + */ +struct WireFeeSet +{ + + /** + * Kept in a DLL. + */ + struct WireFeeSet *next; + + /** + * Kept in a DLL. + */ + struct WireFeeSet *prev; + + /** + * Actual fees. + */ + struct TALER_WireFeeSet fees; + + /** + * Start date of fee validity (inclusive). + */ + struct GNUNET_TIME_Timestamp start_date; + + /** + * End date of fee validity (exclusive). + */ + struct GNUNET_TIME_Timestamp end_date; + + /** + * Wire method the fees apply to. + */ + char *method; +}; + + +/** + * State we keep per thread to cache the /wire response. + */ +struct WireStateHandle +{ + + /** + * JSON reply for /wire response. + */ + json_t *json_reply; + + /** + * ETag for this response (if any). + */ + char *etag; + + /** + * head of DLL of wire fees. + */ + struct WireFeeSet *wfs_head; + + /** + * Tail of DLL of wire fees. + */ + struct WireFeeSet *wfs_tail; + + /** + * Earliest timestamp of all the wire methods when we have no more fees. + */ + struct GNUNET_TIME_Absolute cache_expiration; + + /** + * @e cache_expiration time, formatted. + */ + char dat[128]; + + /** + * For which (global) wire_generation was this data structure created? + * Used to check when we are outdated and need to be re-generated. + */ + uint64_t wire_generation; + + /** + * Is the wire data ready? + */ + bool ready; + +}; + + +/** + * Stores the latest generation of our wire response. + */ +static struct WireStateHandle *wire_state; + +/** + * Handler listening for wire updates by other exchange + * services. + */ +static struct GNUNET_DB_EventHandler *wire_eh; + +/** + * Counter incremented whenever we have a reason to re-build the #wire_state + * because something external changed. + */ +static uint64_t wire_generation; + + +/** * Stores the latest generation of our key state. */ static struct TEH_KeyStateHandle *key_state; /** * Counter incremented whenever we have a reason to re-build the keys because - * something external changed (in another thread). See #TEH_keys_get_state() and + * something external changed. See #TEH_keys_get_state() and * #TEH_keys_update_states() for uses of this variable. */ static uint64_t key_generation; @@ -422,9 +547,19 @@ static struct GNUNET_SCHEDULER_Task *keys_tt; static struct GNUNET_TIME_Relative signkey_legal_duration; /** + * What type of asset are we dealing with here? + */ +static char *asset_type; + +/** * RSA security module public key, all zero if not known. */ -static struct TALER_SecurityModulePublicKeyP denom_sm_pub; +static struct TALER_SecurityModulePublicKeyP denom_rsa_sm_pub; + +/** + * CS security module public key, all zero if not known. + */ +static struct TALER_SecurityModulePublicKeyP denom_cs_sm_pub; /** * EdDSA security module public key, all zero if not known. @@ -438,6 +573,449 @@ static bool terminating; /** + * Free memory associated with @a wsh + * + * @param[in] wsh wire state to destroy + */ +static void +destroy_wire_state (struct WireStateHandle *wsh) +{ + struct WireFeeSet *wfs; + + while (NULL != (wfs = wsh->wfs_head)) + { + GNUNET_CONTAINER_DLL_remove (wsh->wfs_head, + wsh->wfs_tail, + wfs); + GNUNET_free (wfs->method); + GNUNET_free (wfs); + } + json_decref (wsh->json_reply); + GNUNET_free (wsh->etag); + GNUNET_free (wsh); +} + + +/** + * Function called whenever another exchange process has updated + * the wire data in the database. + * + * @param cls NULL + * @param extra unused + * @param extra_size number of bytes in @a extra unused + */ +static void +wire_update_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + (void) cls; + (void) extra; + (void) extra_size; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received /wire update event\n"); + TEH_check_invariants (); + wire_generation++; + key_generation++; + TEH_resume_keys_requests (false); +} + + +enum GNUNET_GenericReturnValue +TEH_wire_init () +{ + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), + }; + + wire_eh = TEH_plugin->event_listen (TEH_plugin->cls, + GNUNET_TIME_UNIT_FOREVER_REL, + &es, + &wire_update_event_cb, + NULL); + if (NULL == wire_eh) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +void +TEH_wire_done () +{ + if (NULL != wire_state) + { + destroy_wire_state (wire_state); + wire_state = NULL; + } + if (NULL != wire_eh) + { + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + wire_eh); + wire_eh = NULL; + } +} + + +/** + * Add information about a wire account to @a cls. + * + * @param cls a `json_t *` object to expand with wire account details + * @param payto_uri the exchange bank account URI to add + * @param conversion_url URL of a conversion service, NULL if there is no conversion + * @param debit_restrictions JSON array with debit restrictions on the account + * @param credit_restrictions JSON array with credit restrictions on the account + * @param master_sig master key signature affirming that this is a bank + * account of the exchange (of purpose #TALER_SIGNATURE_MASTER_WIRE_DETAILS) + * @param bank_label label the wallet should use to display the account, can be NULL + * @param priority priority for ordering bank account labels + */ +static void +add_wire_account (void *cls, + const char *payto_uri, + const char *conversion_url, + const json_t *debit_restrictions, + const json_t *credit_restrictions, + const struct TALER_MasterSignatureP *master_sig, + const char *bank_label, + int64_t priority) +{ + json_t *a = cls; + + if (GNUNET_OK != + TALER_exchange_wire_signature_check ( + payto_uri, + conversion_url, + debit_restrictions, + credit_restrictions, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has wire account with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + if (0 != + json_array_append_new ( + a, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("payto_uri", + payto_uri), + 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_array_incref ("debit_restrictions", + (json_t *) debit_restrictions), + GNUNET_JSON_pack_array_incref ("credit_restrictions", + (json_t *) credit_restrictions), + GNUNET_JSON_pack_data_auto ("master_sig", + master_sig)))) + { + GNUNET_break (0); /* out of memory!? */ + return; + } +} + + +/** + * Closure for #add_wire_fee(). + */ +struct AddContext +{ + /** + * Wire method the fees are for. + */ + char *wire_method; + + /** + * Wire state we are building. + */ + struct WireStateHandle *wsh; + + /** + * Array to append the fee to. + */ + json_t *a; + + /** + * Set to the maximum end-date seen. + */ + struct GNUNET_TIME_Absolute max_seen; +}; + + +/** + * Add information about a wire account to @a cls. + * + * @param cls a `struct AddContext` + * @param fees the wire fees we charge + * @param start_date from when are these fees valid (start date) + * @param end_date until when are these fees valid (end date, exclusive) + * @param master_sig master key signature affirming that this is the correct + * fee (of purpose #TALER_SIGNATURE_MASTER_WIRE_FEES) + */ +static void +add_wire_fee (void *cls, + const struct TALER_WireFeeSet *fees, + struct GNUNET_TIME_Timestamp start_date, + struct GNUNET_TIME_Timestamp end_date, + const struct TALER_MasterSignatureP *master_sig) +{ + struct AddContext *ac = cls; + struct WireFeeSet *wfs; + + if (GNUNET_OK != + TALER_exchange_offline_wire_fee_verify ( + ac->wire_method, + start_date, + end_date, + fees, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has wire fee with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + ac->max_seen = GNUNET_TIME_absolute_max (ac->max_seen, + end_date.abs_time); + wfs = GNUNET_new (struct WireFeeSet); + wfs->start_date = start_date; + wfs->end_date = end_date; + wfs->fees = *fees; + wfs->method = GNUNET_strdup (ac->wire_method); + GNUNET_CONTAINER_DLL_insert (ac->wsh->wfs_head, + ac->wsh->wfs_tail, + wfs); + if (0 != + json_array_append_new ( + ac->a, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("wire_fee", + &fees->wire), + TALER_JSON_pack_amount ("closing_fee", + &fees->closing), + GNUNET_JSON_pack_timestamp ("start_date", + start_date), + GNUNET_JSON_pack_timestamp ("end_date", + end_date), + GNUNET_JSON_pack_data_auto ("sig", + master_sig)))) + { + GNUNET_break (0); /* out of memory!? */ + return; + } +} + + +/** + * Create the /wire response from our database state. + * + * @return NULL on error + */ +static struct WireStateHandle * +build_wire_state (void) +{ + json_t *wire_accounts_array; + json_t *wire_fee_object; + uint64_t wg = wire_generation; /* must be obtained FIRST */ + enum GNUNET_DB_QueryStatus qs; + struct WireStateHandle *wsh; + json_t *wads; + + wsh = GNUNET_new (struct WireStateHandle); + wsh->wire_generation = wg; + wire_accounts_array = json_array (); + GNUNET_assert (NULL != wire_accounts_array); + qs = TEH_plugin->get_wire_accounts (TEH_plugin->cls, + &add_wire_account, + wire_accounts_array); + if (0 > qs) + { + GNUNET_break (0); + json_decref (wire_accounts_array); + wsh->ready = false; + return wsh; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Build /wire data with %u accounts\n", + (unsigned int) json_array_size (wire_accounts_array)); + wire_fee_object = json_object (); + GNUNET_assert (NULL != wire_fee_object); + wsh->cache_expiration = GNUNET_TIME_UNIT_FOREVER_ABS; + { + json_t *account; + size_t index; + + json_array_foreach (wire_accounts_array, + index, + account) + { + char *wire_method; + const char *payto_uri = json_string_value (json_object_get (account, + "payto_uri")); + + GNUNET_assert (NULL != payto_uri); + wire_method = TALER_payto_get_method (payto_uri); + if (NULL == wire_method) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No wire method in `%s'\n", + payto_uri); + wsh->ready = false; + json_decref (wire_accounts_array); + json_decref (wire_fee_object); + return wsh; + } + if (NULL == json_object_get (wire_fee_object, + wire_method)) + { + struct AddContext ac = { + .wire_method = wire_method, + .wsh = wsh, + .a = json_array () + }; + + GNUNET_assert (NULL != ac.a); + qs = TEH_plugin->get_wire_fees (TEH_plugin->cls, + wire_method, + &add_wire_fee, + &ac); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ac.a); + json_decref (wire_fee_object); + json_decref (wire_accounts_array); + GNUNET_free (wire_method); + wsh->ready = false; + return wsh; + } + if (0 != json_array_size (ac.a)) + { + wsh->cache_expiration + = GNUNET_TIME_absolute_min (ac.max_seen, + wsh->cache_expiration); + GNUNET_assert (0 == + json_object_set_new (wire_fee_object, + wire_method, + ac.a)); + } + else + { + json_decref (ac.a); + } + } + GNUNET_free (wire_method); + } + } + + wads = json_array (); /* #7271 */ + GNUNET_assert (NULL != wads); + wsh->json_reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("accounts", + wire_accounts_array), + GNUNET_JSON_pack_array_steal ("wads", + wads), + GNUNET_JSON_pack_object_steal ("fees", + wire_fee_object)); + wsh->ready = true; + return wsh; +} + + +void +TEH_wire_update_state (void) +{ + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_EXCHANGE_WIRE_UPDATED), + }; + + TEH_plugin->event_notify (TEH_plugin->cls, + &es, + NULL, + 0); + wire_generation++; + key_generation++; +} + + +/** + * Return the current key state for this thread. Possibly + * re-builds the key state if we have reason to believe + * that something changed. + * + * @return NULL on error + */ +struct WireStateHandle * +get_wire_state (void) +{ + struct WireStateHandle *old_wsh; + + old_wsh = wire_state; + if ( (NULL == old_wsh) || + (old_wsh->wire_generation < wire_generation) ) + { + struct WireStateHandle *wsh; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Rebuilding /wire, generation upgrade from %llu to %llu\n", + (unsigned long long) (NULL == old_wsh) ? 0LL : + old_wsh->wire_generation, + (unsigned long long) wire_generation); + TEH_check_invariants (); + wsh = build_wire_state (); + wire_state = wsh; + if (NULL != old_wsh) + destroy_wire_state (old_wsh); + TEH_check_invariants (); + return wsh; + } + return old_wsh; +} + + +const struct TALER_WireFeeSet * +TEH_wire_fees_by_time ( + struct GNUNET_TIME_Timestamp ts, + const char *method) +{ + struct WireStateHandle *wsh = get_wire_state (); + + for (struct WireFeeSet *wfs = wsh->wfs_head; + NULL != wfs; + wfs = wfs->next) + { + if (0 != strcmp (method, + wfs->method)) + continue; + if ( (GNUNET_TIME_timestamp_cmp (wfs->start_date, + >, + ts)) || + (GNUNET_TIME_timestamp_cmp (ts, + >=, + wfs->end_date)) ) + continue; + return &wfs->fees; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No wire fees for method `%s' at %s configured\n", + method, + GNUNET_TIME_timestamp2s (ts)); + return NULL; +} + + +/** * Function called to forcefully resume suspended keys requests. * * @param cls unused, NULL @@ -528,7 +1106,7 @@ suspend_request (struct MHD_Connection *connection) * @param value a `struct TEH_DenominationKey` * @return #GNUNET_OK */ -static int +static enum GNUNET_GenericReturnValue check_dk (void *cls, const struct GNUNET_HashCode *hc, void *value) @@ -537,11 +1115,20 @@ check_dk (void *cls, (void) cls; (void) hc; - GNUNET_assert (TALER_DENOMINATION_INVALID != dk->denom_pub.cipher); - if (TALER_DENOMINATION_RSA == dk->denom_pub.cipher) + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + break; + case GNUNET_CRYPTO_BSA_RSA: GNUNET_assert (GNUNET_CRYPTO_rsa_public_key_check ( - dk->denom_pub.details.rsa_public_key)); - return GNUNET_OK; + dk->denom_pub.bsign_pub_key->details.rsa_public_key)); + return GNUNET_OK; + case GNUNET_CRYPTO_BSA_CS: + /* nothing to do for GNUNET_CRYPTO_BSA_CS */ + return GNUNET_OK; + } + GNUNET_assert (0); + return GNUNET_SYSERR; } @@ -595,6 +1182,7 @@ clear_response_cache (struct TEH_KeyStateHandle *ksh) MHD_destroy_response (krd->response_compressed); MHD_destroy_response (krd->response_uncompressed); + GNUNET_free (krd->etag); } GNUNET_array_grow (ksh->krd_array, ksh->krd_array_length, @@ -609,19 +1197,43 @@ clear_response_cache (struct TEH_KeyStateHandle *ksh) * @param sm_pub RSA security module public key to check */ static void -check_denom_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) +check_denom_rsa_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) { if (0 != GNUNET_memcmp (sm_pub, - &denom_sm_pub)) + &denom_rsa_sm_pub)) { - if (! GNUNET_is_zero (&denom_sm_pub)) + if (! GNUNET_is_zero (&denom_rsa_sm_pub)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Our RSA security module changed its key. This must not happen.\n"); GNUNET_assert (0); } - denom_sm_pub = *sm_pub; /* TOFU ;-) */ + denom_rsa_sm_pub = *sm_pub; /* TOFU ;-) */ + } +} + + +/** + * Check that the given CS security module's public key is the one + * we have pinned. If it does not match, we die hard. + * + * @param sm_pub RSA security module public key to check + */ +static void +check_denom_cs_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) +{ + if (0 != + GNUNET_memcmp (sm_pub, + &denom_cs_sm_pub)) + { + if (! GNUNET_is_zero (&denom_cs_sm_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Our CS security module changed its key. This must not happen.\n"); + GNUNET_assert (0); + } + denom_cs_sm_pub = *sm_pub; /* TOFU ;-) */ } } @@ -659,7 +1271,7 @@ check_esign_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) * @param value the `struct HelperDenomination` to release * @return #GNUNET_OK (continue to iterate) */ -static int +static enum GNUNET_GenericReturnValue free_denom_cb (void *cls, const struct GNUNET_HashCode *h_denom_pub, void *value) @@ -684,7 +1296,7 @@ free_denom_cb (void *cls, * @param value the `struct HelperSignkey` to release * @return #GNUNET_OK (continue to iterate) */ -static int +static enum GNUNET_GenericReturnValue free_esign_cb (void *cls, const struct GNUNET_PeerIdentity *pid, void *value) @@ -712,6 +1324,8 @@ destroy_key_helpers (struct HelperState *hs) hs); GNUNET_CONTAINER_multihashmap_destroy (hs->rsa_keys); hs->rsa_keys = NULL; + GNUNET_CONTAINER_multihashmap_destroy (hs->cs_keys); + hs->cs_keys = NULL; GNUNET_CONTAINER_multihashmap_destroy (hs->denom_keys); hs->denom_keys = NULL; GNUNET_CONTAINER_multipeermap_iterate (hs->esign_keys, @@ -719,10 +1333,15 @@ destroy_key_helpers (struct HelperState *hs) hs); GNUNET_CONTAINER_multipeermap_destroy (hs->esign_keys); hs->esign_keys = NULL; - if (NULL != hs->dh) + if (NULL != hs->rsadh) { - TALER_CRYPTO_helper_rsa_disconnect (hs->dh); - hs->dh = NULL; + TALER_CRYPTO_helper_rsa_disconnect (hs->rsadh); + hs->rsadh = NULL; + } + if (NULL != hs->csdh) + { + TALER_CRYPTO_helper_cs_disconnect (hs->csdh); + hs->csdh = NULL; } if (NULL != hs->esh) { @@ -740,46 +1359,40 @@ destroy_key_helpers (struct HelperState *hs) * denomination. */ static struct TALER_AgeMask -load_age_mask (const char*section_name) +load_age_mask (const char *section_name) { static const struct TALER_AgeMask null_mask = {0}; - struct TALER_AgeMask age_mask = {0}; - const struct TALER_Extension *age_ext = - TEH_extensions[TALER_Extension_AgeRestriction]; + enum GNUNET_GenericReturnValue ret; - // Get the age mask from the extension, if configured - if (NULL != age_ext->config) - { - age_mask = *(struct TALER_AgeMask *) age_ext->config; - } + if (GNUNET_OK != (GNUNET_CONFIGURATION_have_value ( + TEH_cfg, + section_name, + "AGE_RESTRICTED"))) + return null_mask; - if (age_mask.mask == 0) + if (GNUNET_SYSERR == + (ret = GNUNET_CONFIGURATION_get_value_yesno (TEH_cfg, + section_name, + "AGE_RESTRICTED"))) { - /* Age restriction support is not enabled. Ignore the AGE_RESTRICTED field - * for the particular denomination and simply return the null_mask - */ + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section_name, + "AGE_RESTRICTED", + "Value must be YES or NO\n"); return null_mask; } - if (GNUNET_OK == (GNUNET_CONFIGURATION_have_value ( - TEH_cfg, - section_name, - "AGE_RESTRICTED"))) + if (GNUNET_OK == ret) { - enum GNUNET_GenericReturnValue ret; - if (GNUNET_SYSERR == (ret = GNUNET_CONFIGURATION_get_value_yesno (TEH_cfg, - section_name, - "AGE_RESTRICTED"))) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - section_name, - "AGE_RESTRICTED", - "Value must be YES or NO\n"); - return null_mask; - } + if (! TEH_age_restriction_enabled) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "age restriction set in section %s, yet, age restriction is not enabled\n", + section_name); + return TEH_age_restriction_config.mask; } - return age_mask; + + return null_mask; } @@ -795,8 +1408,8 @@ load_age_mask (const char*section_name) * zero if the key has been revoked or purged * @param validity_duration how long does the key remain available for signing; * zero if the key has been revoked or purged - * @param h_denom_pub hash of the @a denom_pub that is available (or was purged) - * @param denom_pub the public key itself, NULL if the key was revoked or purged + * @param h_rsa hash of the @a denom_pub that is available (or was purged) + * @param bs_pub the public key itself, NULL if the key was revoked or purged * @param sm_pub public key of the security module, NULL if the key was revoked or purged * @param sm_sig signature from the security module, NULL if the key was revoked or purged * The signature was already verified against @a sm_pub. @@ -808,7 +1421,7 @@ helper_rsa_cb ( struct GNUNET_TIME_Timestamp start_time, struct GNUNET_TIME_Relative validity_duration, const struct TALER_RsaPubHashP *h_rsa, - const struct TALER_DenominationPublicKey *denom_pub, + struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub, const struct TALER_SecurityModulePublicKeyP *sm_pub, const struct TALER_SecurityModuleSignatureP *sm_sig) { @@ -832,16 +1445,15 @@ helper_rsa_cb ( return; } GNUNET_assert (NULL != sm_pub); - check_denom_sm_pub (sm_pub); + check_denom_rsa_sm_pub (sm_pub); hd = GNUNET_new (struct HelperDenomination); hd->start_time = start_time; hd->validity_duration = validity_duration; hd->h_details.h_rsa = *h_rsa; hd->sm_sig = *sm_sig; - GNUNET_assert (TALER_DENOMINATION_RSA == denom_pub->cipher); - TALER_denom_pub_deep_copy (&hd->denom_pub, - denom_pub); - GNUNET_assert (TALER_DENOMINATION_RSA == hd->denom_pub.cipher); + GNUNET_assert (GNUNET_CRYPTO_BSA_RSA == bs_pub->cipher); + hd->denom_pub.bsign_pub_key = + GNUNET_CRYPTO_bsign_pub_incref (bs_pub); /* load the age mask for the denomination, if applicable */ hd->denom_pub.age_mask = load_age_mask (section_name); TALER_denom_pub_hash (&hd->denom_pub, @@ -865,6 +1477,86 @@ helper_rsa_cb ( /** + * Function called with information about available CS keys for signing. Usually + * only called once per key upon connect. Also called again in case a key is + * being revoked, in that case with an @a end_time of zero. + * + * @param cls closure with the `struct HelperState *` + * @param section_name name of the denomination type in the configuration; + * NULL if the key has been revoked or purged + * @param start_time when does the key become available for signing; + * zero if the key has been revoked or purged + * @param validity_duration how long does the key remain available for signing; + * zero if the key has been revoked or purged + * @param h_cs hash of the @a denom_pub that is available (or was purged) + * @param bs_pub the public key itself, NULL if the key was revoked or purged + * @param sm_pub public key of the security module, NULL if the key was revoked or purged + * @param sm_sig signature from the security module, NULL if the key was revoked or purged + * The signature was already verified against @a sm_pub. + */ +static void +helper_cs_cb ( + void *cls, + const char *section_name, + struct GNUNET_TIME_Timestamp start_time, + struct GNUNET_TIME_Relative validity_duration, + const struct TALER_CsPubHashP *h_cs, + struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub, + const struct TALER_SecurityModulePublicKeyP *sm_pub, + const struct TALER_SecurityModuleSignatureP *sm_sig) +{ + struct HelperState *hs = cls; + struct HelperDenomination *hd; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "CS helper announces key %s for denomination type %s with validity %s\n", + GNUNET_h2s (&h_cs->hash), + section_name, + GNUNET_STRINGS_relative_time_to_string (validity_duration, + GNUNET_NO)); + key_generation++; + TEH_resume_keys_requests (false); + hd = GNUNET_CONTAINER_multihashmap_get (hs->cs_keys, + &h_cs->hash); + if (NULL != hd) + { + /* should be just an update (revocation!), so update existing entry */ + hd->validity_duration = validity_duration; + return; + } + GNUNET_assert (NULL != sm_pub); + check_denom_cs_sm_pub (sm_pub); + hd = GNUNET_new (struct HelperDenomination); + hd->start_time = start_time; + hd->validity_duration = validity_duration; + hd->h_details.h_cs = *h_cs; + hd->sm_sig = *sm_sig; + GNUNET_assert (GNUNET_CRYPTO_BSA_CS == bs_pub->cipher); + hd->denom_pub.bsign_pub_key + = GNUNET_CRYPTO_bsign_pub_incref (bs_pub); + /* load the age mask for the denomination, if applicable */ + hd->denom_pub.age_mask = load_age_mask (section_name); + TALER_denom_pub_hash (&hd->denom_pub, + &hd->h_denom_pub); + hd->section_name = GNUNET_strdup (section_name); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + hs->denom_keys, + &hd->h_denom_pub.hash, + hd, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + hs->cs_keys, + &hd->h_details.h_cs.hash, + hd, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); +} + + +/** * Function called with information about available keys for signing. Usually * only called once per key upon connect. Also called again in case a key is * being revoked, in that case with an @a end_time of zero. @@ -940,18 +1632,32 @@ setup_key_helpers (struct HelperState *hs) hs->rsa_keys = GNUNET_CONTAINER_multihashmap_create (1024, GNUNET_YES); + hs->cs_keys + = GNUNET_CONTAINER_multihashmap_create (1024, + GNUNET_YES); hs->esign_keys = GNUNET_CONTAINER_multipeermap_create (32, GNUNET_NO /* MUST BE NO! */); - hs->dh = TALER_CRYPTO_helper_rsa_connect (TEH_cfg, - &helper_rsa_cb, - hs); - if (NULL == hs->dh) + hs->rsadh = TALER_CRYPTO_helper_rsa_connect (TEH_cfg, + "taler-exchange", + &helper_rsa_cb, + hs); + if (NULL == hs->rsadh) + { + destroy_key_helpers (hs); + return GNUNET_SYSERR; + } + hs->csdh = TALER_CRYPTO_helper_cs_connect (TEH_cfg, + "taler-exchange", + &helper_cs_cb, + hs); + if (NULL == hs->csdh) { destroy_key_helpers (hs); return GNUNET_SYSERR; } hs->esh = TALER_CRYPTO_helper_esign_connect (TEH_cfg, + "taler-exchange", &helper_esign_cb, hs); if (NULL == hs->esh) @@ -971,7 +1677,8 @@ setup_key_helpers (struct HelperState *hs) static void sync_key_helpers (struct HelperState *hs) { - TALER_CRYPTO_helper_rsa_poll (hs->dh); + TALER_CRYPTO_helper_rsa_poll (hs->rsadh); + TALER_CRYPTO_helper_cs_poll (hs->csdh); TALER_CRYPTO_helper_esign_poll (hs->esh); } @@ -984,7 +1691,7 @@ sync_key_helpers (struct HelperState *hs) * @param value a `struct TEH_DenominationKey` to free * @return #GNUNET_OK (continue to iterate) */ -static int +static enum GNUNET_GenericReturnValue clear_denomination_cb (void *cls, const struct GNUNET_HashCode *h_denom_pub, void *value) @@ -1015,7 +1722,7 @@ clear_denomination_cb (void *cls, * @param value a `struct SigningKey` to free * @return #GNUNET_OK (continue to iterate) */ -static int +static enum GNUNET_GenericReturnValue clear_signkey_cb (void *cls, const struct GNUNET_PeerIdentity *pid, void *value) @@ -1040,7 +1747,16 @@ static void destroy_key_state (struct TEH_KeyStateHandle *ksh, bool free_helper) { + struct TEH_GlobalFee *gf; + clear_response_cache (ksh); + while (NULL != (gf = ksh->gf_head)) + { + GNUNET_CONTAINER_DLL_remove (ksh->gf_head, + ksh->gf_tail, + gf); + GNUNET_free (gf); + } GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, &clear_denomination_cb, ksh); @@ -1051,6 +1767,8 @@ destroy_key_state (struct TEH_KeyStateHandle *ksh, GNUNET_CONTAINER_multipeermap_destroy (ksh->signkey_map); json_decref (ksh->auditors); ksh->auditors = NULL; + json_decref (ksh->global_fees); + ksh->global_fees = NULL; if (free_helper) { destroy_key_helpers (ksh->helpers); @@ -1109,6 +1827,17 @@ TEH_keys_init () "SIGNKEY_LEGAL_DURATION"); return GNUNET_SYSERR; } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (TEH_cfg, + "exchange", + "ASSET_TYPE", + &asset_type)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "exchange", + "ASSET_TYPE"); + asset_type = GNUNET_strdup ("fiat"); + } keys_eh = TEH_plugin->event_listen (TEH_plugin->cls, GNUNET_TIME_UNIT_FOREVER_REL, &es, @@ -1161,7 +1890,7 @@ static void denomination_info_cb ( void *cls, const struct TALER_DenominationPublicKey *denom_pub, - const struct TALER_DenominationHash *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta, const struct TALER_MasterSignatureP *master_sig, bool recoup_possible) @@ -1169,7 +1898,25 @@ denomination_info_cb ( struct TEH_KeyStateHandle *ksh = cls; struct TEH_DenominationKey *dk; - GNUNET_assert (TALER_DENOMINATION_INVALID != denom_pub->cipher); + if (GNUNET_OK != + TALER_exchange_offline_denom_validity_verify ( + h_denom_pub, + meta->start, + meta->expire_withdraw, + meta->expire_deposit, + meta->expire_legal, + &meta->value, + &meta->fees, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has denomination with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + + GNUNET_assert (GNUNET_CRYPTO_BSA_INVALID != + denom_pub->bsign_pub_key->cipher); if (GNUNET_TIME_absolute_is_zero (meta->start.abs_time) || GNUNET_TIME_absolute_is_zero (meta->expire_withdraw.abs_time) || GNUNET_TIME_absolute_is_zero (meta->expire_deposit.abs_time) || @@ -1181,12 +1928,14 @@ denomination_info_cb ( return; } dk = GNUNET_new (struct TEH_DenominationKey); - TALER_denom_pub_deep_copy (&dk->denom_pub, - denom_pub); + TALER_denom_pub_copy (&dk->denom_pub, + denom_pub); dk->h_denom_pub = *h_denom_pub; dk->meta = *meta; dk->master_sig = *master_sig; dk->recoup_possible = recoup_possible; + dk->denom_pub.age_mask = meta->age_mask; + GNUNET_assert ( GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (ksh->denomkey_map, @@ -1215,6 +1964,19 @@ signkey_info_cb ( struct SigningKey *sk; struct GNUNET_PeerIdentity pid; + if (GNUNET_OK != + TALER_exchange_offline_signkey_validity_verify ( + exchange_pub, + meta->start, + meta->expire_sign, + meta->expire_legal, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has signing key with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } sk = GNUNET_new (struct SigningKey); sk->exchange_pub = *exchange_pub; sk->meta = *meta; @@ -1255,7 +2017,7 @@ struct GetAuditorSigsContext * @param value a `struct TEH_DenominationKey` * @return #GNUNET_OK (continue to iterate) */ -static int +static enum GNUNET_GenericReturnValue get_auditor_sigs (void *cls, const struct GNUNET_HashCode *h_denom_pub, void *value) @@ -1336,7 +2098,7 @@ static void auditor_denom_cb ( void *cls, const struct TALER_AuditorPublicKeyP *auditor_pub, - const struct TALER_DenominationHash *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_AuditorSignatureP *auditor_sig) { struct TEH_KeyStateHandle *ksh = cls; @@ -1388,7 +2150,7 @@ struct SignKeyCtx * @param value a `struct SigningKey` * @return #GNUNET_OK (continue to iterate) */ -static int +static enum GNUNET_GenericReturnValue add_sign_key_cb (void *cls, const struct GNUNET_PeerIdentity *pid, void *value) @@ -1457,7 +2219,7 @@ struct DenomKeyCtx * @param value a `struct TEH_DenominationKey` * @return #GNUNET_OK (continue to iterate) */ -static int +static enum GNUNET_GenericReturnValue add_denom_key_cb (void *cls, const struct GNUNET_HashCode *h_denom_pub, void *value) @@ -1494,68 +2256,16 @@ add_denom_key_cb (void *cls, /** - * Produce HTTP "Date:" header. - * - * @param at time to write to @a date - * @param[out] date where to write the header, with - * at least 128 bytes available space. - */ -static void -get_date_string (struct GNUNET_TIME_Absolute at, - char date[128]) -{ - static const char *const days[] = - { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; - static const char *const mons[] = - { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", - "Nov", "Dec"}; - struct tm now; - time_t t; -#if ! defined(HAVE_C11_GMTIME_S) && ! defined(HAVE_W32_GMTIME_S) && \ - ! defined(HAVE_GMTIME_R) - struct tm*pNow; -#endif - - date[0] = 0; - t = (time_t) (at.abs_value_us / 1000LL / 1000LL); -#if defined(HAVE_C11_GMTIME_S) - if (NULL == gmtime_s (&t, &now)) - return; -#elif defined(HAVE_W32_GMTIME_S) - if (0 != gmtime_s (&now, &t)) - return; -#elif defined(HAVE_GMTIME_R) - if (NULL == gmtime_r (&t, &now)) - return; -#else - pNow = gmtime (&t); - if (NULL == pNow) - return; - now = *pNow; -#endif - sprintf (date, - "%3s, %02u %3s %04u %02u:%02u:%02u GMT", - days[now.tm_wday % 7], - (unsigned int) now.tm_mday, - mons[now.tm_mon % 12], - (unsigned int) (1900 + now.tm_year), - (unsigned int) now.tm_hour, - (unsigned int) now.tm_min, - (unsigned int) now.tm_sec); -} - - -/** * Add the headers we want to set for every /keys response. * - * @param ksh the key state to use + * @param cls the key state to use * @param[in,out] response the response to modify - * @return #GNUNET_OK on success */ -static enum GNUNET_GenericReturnValue -setup_general_response_headers (struct TEH_KeyStateHandle *ksh, +static void +setup_general_response_headers (void *cls, struct MHD_Response *response) { + struct TEH_KeyStateHandle *ksh = cls; char dat[128]; TALER_MHD_add_global_headers (response); @@ -1563,27 +2273,38 @@ setup_general_response_headers (struct TEH_KeyStateHandle *ksh, MHD_add_response_header (response, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json")); - get_date_string (ksh->reload_time.abs_time, - dat); GNUNET_break (MHD_YES == MHD_add_response_header (response, - MHD_HTTP_HEADER_LAST_MODIFIED, - dat)); + MHD_HTTP_HEADER_CACHE_CONTROL, + "public,must-revalidate,max-age=86400")); if (! GNUNET_TIME_relative_is_zero (ksh->rekey_frequency)) { struct GNUNET_TIME_Relative r; struct GNUNET_TIME_Absolute a; + struct GNUNET_TIME_Timestamp km; struct GNUNET_TIME_Timestamp m; + struct GNUNET_TIME_Timestamp we; r = GNUNET_TIME_relative_min (TEH_max_keys_caching, ksh->rekey_frequency); a = GNUNET_TIME_relative_to_absolute (r); - m = GNUNET_TIME_absolute_to_timestamp (a); - get_date_string (m.abs_time, - dat); + /* Round up to next full day to ensure the expiration + time does not become a fingerprint! */ + a = GNUNET_TIME_absolute_round_down (a, + GNUNET_TIME_UNIT_DAYS); + a = GNUNET_TIME_absolute_add (a, + GNUNET_TIME_UNIT_DAYS); + km = GNUNET_TIME_absolute_to_timestamp (a); + we = GNUNET_TIME_absolute_to_timestamp (wire_state->cache_expiration); + m = GNUNET_TIME_timestamp_min (we, + km); + TALER_MHD_get_date_string (m.abs_time, + dat); GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Setting /keys 'Expires' header to '%s'\n", - dat); + "Setting /keys 'Expires' header to '%s' (rekey frequency is %s)\n", + dat, + GNUNET_TIME_relative2s (ksh->rekey_frequency, + false)); GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_EXPIRES, @@ -1592,7 +2313,32 @@ setup_general_response_headers (struct TEH_KeyStateHandle *ksh, = GNUNET_TIME_timestamp_min (m, ksh->signature_expires); } - return GNUNET_OK; + /* Set cache control headers: our response varies depending on these headers */ + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_VARY, + MHD_HTTP_HEADER_ACCEPT_ENCODING)); +} + + +/** + * Function called with wallet balance thresholds. + * + * @param[in,out] cls a `json **` where to put the array of json amounts discovered + * @param threshold another threshold amount to add + */ +static void +wallet_threshold_cb (void *cls, + const struct TALER_Amount *threshold) +{ + json_t **ret = cls; + + if (NULL == *ret) + *ret = json_array (); + GNUNET_assert (0 == + json_array_append_new (*ret, + TALER_JSON_from_amount ( + threshold))); } @@ -1601,52 +2347,58 @@ setup_general_response_headers (struct TEH_KeyStateHandle *ksh, * @a recoup and @a denoms. * * @param[in,out] ksh key state handle we build @a krd for - * @param[in] denom_keys_hash hash over all the denominatoin keys in @a denoms - * @param last_cpd timestamp to use - * @param signkeys list of sign keys to return - * @param recoup list of revoked keys to return - * @param denoms list of denominations to return - * @param age_restricted_denoms list of age restricted denominations to return, can be NULL + * @param[in] denom_keys_hash hash over all the denomination keys in @a denoms + * @param last_cherry_pick_date timestamp to use + * @param[in,out] signkeys list of sign keys to return + * @param[in,out] recoup list of revoked keys to return + * @param[in,out] grouped_denominations list of grouped denominations to return * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue create_krd (struct TEH_KeyStateHandle *ksh, const struct GNUNET_HashCode *denom_keys_hash, - struct GNUNET_TIME_Timestamp last_cpd, + struct GNUNET_TIME_Timestamp last_cherry_pick_date, json_t *signkeys, json_t *recoup, - json_t *denoms, - json_t *age_restricted_denoms) + json_t *grouped_denominations) { struct KeysResponseData krd; struct TALER_ExchangePublicKeyP exchange_pub; struct TALER_ExchangeSignatureP exchange_sig; + struct WireStateHandle *wsh; json_t *keys; - GNUNET_assert (! GNUNET_TIME_absolute_is_zero (last_cpd.abs_time)); + wsh = get_wire_state (); + if (! wsh->ready) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_assert (! GNUNET_TIME_absolute_is_zero ( + last_cherry_pick_date.abs_time)); GNUNET_assert (NULL != signkeys); GNUNET_assert (NULL != recoup); - GNUNET_assert (NULL != denoms); + GNUNET_assert (NULL != grouped_denominations); GNUNET_assert (NULL != ksh->auditors); GNUNET_assert (NULL != TEH_currency); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Creating /keys at cherry pick date %s\n", - GNUNET_TIME_timestamp2s (last_cpd)); - /* Sign hash over denomination keys */ - { - struct TALER_ExchangeKeySetPS ks = { - .purpose.size = htonl (sizeof (ks)), - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_KEY_SET), - .list_issue_date = GNUNET_TIME_timestamp_hton (last_cpd), - .hc = *denom_keys_hash - }; + GNUNET_TIME_timestamp2s (last_cherry_pick_date)); + + /* Sign hash over master signatures of all denomination keys until this time + (in reverse order). */ + { enum TALER_ErrorCode ec; if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign2 (ksh, - &ks, - &exchange_pub, - &exchange_sig))) + (ec = + TALER_exchange_online_key_set_sign ( + &TEH_keys_exchange_sign2_, + ksh, + last_cherry_pick_date, + denom_keys_hash, + &exchange_pub, + &exchange_sig))) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Could not create key response data: cannot sign (%s)\n", @@ -1654,6 +2406,7 @@ create_krd (struct TEH_KeyStateHandle *ksh, return GNUNET_SYSERR; } } + { const struct SigningKey *sk; @@ -1664,11 +2417,33 @@ create_krd (struct TEH_KeyStateHandle *ksh, ksh->signature_expires); } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Build /keys data with %u wire accounts\n", + (unsigned int) json_array_size ( + json_object_get (wsh->json_reply, + "accounts"))); + keys = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("version", EXCHANGE_PROTOCOL_VERSION), + GNUNET_JSON_pack_string ("base_url", + TEH_base_url), GNUNET_JSON_pack_string ("currency", TEH_currency), + GNUNET_JSON_pack_object_steal ( + "currency_specification", + TALER_CONFIG_currency_specs_to_json (TEH_cspec)), + TALER_JSON_pack_amount ("stefan_abs", + &TEH_stefan_abs), + TALER_JSON_pack_amount ("stefan_log", + &TEH_stefan_log), + GNUNET_JSON_pack_double ("stefan_lin", + (double) TEH_stefan_lin), + GNUNET_JSON_pack_string ("asset_type", + asset_type), + GNUNET_JSON_pack_bool ("rewards_allowed", + GNUNET_YES == + TEH_enable_rewards), GNUNET_JSON_pack_data_auto ("master_public_key", &TEH_master_public_key), GNUNET_JSON_pack_time_rel ("reserve_closing_delay", @@ -1677,71 +2452,115 @@ create_krd (struct TEH_KeyStateHandle *ksh, signkeys), GNUNET_JSON_pack_array_incref ("recoup", recoup), - GNUNET_JSON_pack_array_incref ("denoms", - denoms), + GNUNET_JSON_pack_array_incref ("wads", + json_object_get (wsh->json_reply, + "wads")), + GNUNET_JSON_pack_array_incref ("accounts", + json_object_get (wsh->json_reply, + "accounts")), + GNUNET_JSON_pack_object_incref ("wire_fees", + json_object_get (wsh->json_reply, + "fees")), + GNUNET_JSON_pack_array_incref ("denominations", + grouped_denominations), GNUNET_JSON_pack_array_incref ("auditors", ksh->auditors), + GNUNET_JSON_pack_array_incref ("global_fees", + ksh->global_fees), GNUNET_JSON_pack_timestamp ("list_issue_date", - last_cpd), - GNUNET_JSON_pack_data_auto ("eddsa_pub", + last_cherry_pick_date), + GNUNET_JSON_pack_data_auto ("exchange_pub", &exchange_pub), - GNUNET_JSON_pack_data_auto ("eddsa_sig", + GNUNET_JSON_pack_data_auto ("exchange_sig", &exchange_sig)); GNUNET_assert (NULL != keys); - // Set wallet limit if KYC is configured - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (GNUNET_OK == - TALER_amount_is_valid (&TEH_kyc_config.wallet_balance_limit)) ) + /* Set wallet limit if KYC is configured */ { - GNUNET_assert ( - 0 == - json_object_set_new ( - keys, - "wallet_balance_limit_without_kyc", - TALER_JSON_from_amount ( - &TEH_kyc_config.wallet_balance_limit))); - } - - // Signal support for the age-restriction extension, if so configured, and - // add the array of age-restricted denominations. - if (TEH_extension_enabled (TALER_Extension_AgeRestriction) && - NULL != age_restricted_denoms) - { - struct TALER_AgeMask *mask; - json_t *config; - - mask = (struct - TALER_AgeMask *) TEH_extensions[TALER_Extension_AgeRestriction]-> - config; - config = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_bool ("critical", false), - GNUNET_JSON_pack_string ("version", "1"), - GNUNET_JSON_pack_string ("age_groups", TALER_age_mask_to_string (mask))); - GNUNET_assert (NULL != config); - GNUNET_assert ( - 0 == - json_object_set_new ( - keys, - "age_restriction", - config)); + json_t *wblwk = NULL; + + TALER_KYCLOGIC_kyc_iterate_thresholds ( + TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, + &wallet_threshold_cb, + &wblwk); + if (NULL != wblwk) + GNUNET_assert ( + 0 == + json_object_set_new ( + keys, + "wallet_balance_limit_without_kyc", + wblwk)); + } - GNUNET_assert ( - 0 == - json_object_set_new ( + /* Signal support for the configured, enabled extensions. */ + { + json_t *extensions = json_object (); + bool has_extensions = false; + + GNUNET_assert (NULL != extensions); + /* Fill in the configurations of the enabled extensions */ + for (const struct TALER_Extensions *iter = TALER_extensions_get_head (); + NULL != iter && NULL != iter->extension; + iter = iter->next) + { + const struct TALER_Extension *extension = iter->extension; + json_t *manifest; + int r; + + /* skip if not enabled */ + if (! extension->enabled) + continue; + + /* flag our findings so far */ + has_extensions = true; + + + manifest = extension->manifest (extension); + GNUNET_assert (manifest); + + r = json_object_set_new ( + extensions, + extension->name, + manifest); + GNUNET_assert (0 == r); + } + + /* Update the keys object with the extensions and its signature */ + if (has_extensions) + { + json_t *sig; + int r; + + r = json_object_set_new ( keys, - "age_restricted_denoms", - age_restricted_denoms)); + "extensions", + extensions); + GNUNET_assert (0 == r); + + /* Add the signature of the extensions, if it is not zero */ + if (TEH_extensions_signed) + { + sig = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("extensions_sig", + &TEH_extensions_sig)); + + r = json_object_update (keys, sig); + GNUNET_assert (0 == r); + } + } + else + { + json_decref (extensions); + } } - // TODO: signal support and configuration for the P2P extension, once - // implemented. { char *keys_json; void *keys_jsonz; size_t keys_jsonz_size; int comp; + char etag[sizeof (struct GNUNET_HashCode) * 2]; /* Convert /keys response to UTF8-String */ keys_json = json_dumps (keys, @@ -1753,15 +2572,34 @@ create_krd (struct TEH_KeyStateHandle *ksh, keys_jsonz = GNUNET_strdup (keys_json); keys_jsonz_size = strlen (keys_json); + /* hash to compute etag */ + { + struct GNUNET_HashCode ehash; + char *end; + + GNUNET_CRYPTO_hash (keys_jsonz, + keys_jsonz_size, + &ehash); + end = GNUNET_STRINGS_data_to_string (&ehash, + sizeof (ehash), + etag, + sizeof (etag)); + *end = '\0'; + } + /* Create uncompressed response */ krd.response_uncompressed = MHD_create_response_from_buffer (keys_jsonz_size, keys_json, MHD_RESPMEM_MUST_FREE); GNUNET_assert (NULL != krd.response_uncompressed); - GNUNET_assert (GNUNET_OK == - setup_general_response_headers (ksh, - krd.response_uncompressed)); + setup_general_response_headers (ksh, + krd.response_uncompressed); + /* Information is always public, revalidate after 1 day */ + GNUNET_break (MHD_YES == + MHD_add_response_header (krd.response_uncompressed, + MHD_HTTP_HEADER_ETAG, + etag)); /* Also compute compressed version of /keys response */ comp = TALER_MHD_body_compress (&keys_jsonz, &keys_jsonz_size); @@ -1777,11 +2615,16 @@ create_krd (struct TEH_KeyStateHandle *ksh, MHD_add_response_header (krd.response_compressed, MHD_HTTP_HEADER_CONTENT_ENCODING, "deflate")) ); - GNUNET_assert (GNUNET_OK == - setup_general_response_headers (ksh, - krd.response_compressed)); + setup_general_response_headers (ksh, + krd.response_compressed); + /* Information is always public, revalidate after 1 day */ + GNUNET_break (MHD_YES == + MHD_add_response_header (krd.response_compressed, + MHD_HTTP_HEADER_ETAG, + etag)); + krd.etag = GNUNET_strdup (etag); } - krd.cherry_pick_date = last_cpd; + krd.cherry_pick_date = last_cherry_pick_date; GNUNET_array_append (ksh->krd_array, ksh->krd_array_length, krd); @@ -1790,6 +2633,194 @@ create_krd (struct TEH_KeyStateHandle *ksh, /** + * Element in the `struct SignatureContext` array. + */ +struct SignatureElement +{ + + /** + * Offset of the denomination in the group array, + * for sorting (2nd rank, ascending). + */ + unsigned int offset; + + /** + * Offset of the group in the denominations array, + * for sorting (2nd rank, ascending). + */ + unsigned int group_offset; + + /** + * Pointer to actual master signature to hash over. + */ + struct TALER_MasterSignatureP master_sig; +}; + +/** + * Context for collecting the array of master signatures + * needed to verify the exchange_sig online signature. + */ +struct SignatureContext +{ + /** + * Array of signatures to hash over. + */ + struct SignatureElement *elements; + + /** + * Write offset in the @e elements array. + */ + unsigned int elements_pos; + + /** + * Allocated space for @e elements. + */ + unsigned int elements_size; +}; + + +/** + * Determine order to sort two elements by before + * we hash the master signatures. Used for + * sorting with qsort(). + * + * @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. + */ +static int +signature_context_sort_cb (const void *a, + const void *b) +{ + const struct SignatureElement *sa = a; + const struct SignatureElement *sb = b; + + 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; +} + + +/** + * Append a @a master_sig to the @a sig_ctx using the + * given attributes for (later) sorting. + * + * @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 + */ +static void +append_signature (struct SignatureContext *sig_ctx, + unsigned int group_offset, + unsigned int offset, + const struct TALER_MasterSignatureP *master_sig) +{ + struct SignatureElement *element; + unsigned int new_size; + + if (sig_ctx->elements_pos == sig_ctx->elements_size) + { + 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); + } + element = &sig_ctx->elements[sig_ctx->elements_pos++]; + element->offset = offset; + element->group_offset = group_offset; + element->master_sig = *master_sig; +} + + +/** + *GroupData is the value we store for each group meta-data */ +struct GroupData +{ + /** + * The json blob with the group meta-data and list of denominations + */ + json_t *json; + + /** + * List of denominations for the group, + * included in @e json, do not free separately! + */ + json_t *list; + + /** + * Offset of the group in the final array. + */ + unsigned int group_off; + +}; + + +/** + * Helper function called to clean up the group data + * in the denominations_by_group below. + * + * @param cls unused + * @param key unused + * @param value a `struct GroupData` to free + * @return #GNUNET_OK + */ +static int +free_group (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct GroupData *gd = value; + + (void) cls; + (void) key; + GNUNET_free (gd); + return GNUNET_OK; +} + + +static void +compute_msig_hash (struct SignatureContext *sig_ctx, + struct GNUNET_HashCode *hc) +{ + struct GNUNET_HashContext *hash_context; + + 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_CRYPTO_hash_context_finish (hash_context, + hc); +} + + +/** * Update the "/keys" responses in @a ksh, computing the detailed replies. * * This function is to recompute all (including cherry-picked) responses we @@ -1801,22 +2832,50 @@ create_krd (struct TEH_KeyStateHandle *ksh, static enum GNUNET_GenericReturnValue finish_keys_response (struct TEH_KeyStateHandle *ksh) { + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; json_t *recoup; - struct SignKeyCtx sctx; - json_t *denoms = NULL; - json_t *age_restricted_denoms = NULL; - struct GNUNET_TIME_Timestamp last_cpd; + struct SignKeyCtx sctx = { + .min_sk_frequency = GNUNET_TIME_UNIT_FOREVER_REL + }; + json_t *grouped_denominations = NULL; + struct GNUNET_TIME_Timestamp last_cherry_pick_date; struct GNUNET_CONTAINER_Heap *heap; - struct GNUNET_HashContext *hash_context; + struct SignatureContext sig_ctx = { 0 }; + /* Remember if we have any denomination with age restriction */ + bool has_age_restricted_denomination = false; + struct WireStateHandle *wsh; + wsh = get_wire_state (); + if (! wsh->ready) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 == + json_array_size (json_object_get (wsh->json_reply, + "accounts")) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No wire accounts available. Refusing to generate /keys response.\n"); + return GNUNET_NO; + } sctx.signkeys = json_array (); GNUNET_assert (NULL != sctx.signkeys); - sctx.min_sk_frequency = GNUNET_TIME_UNIT_FOREVER_REL; + recoup = json_array (); + GNUNET_assert (NULL != recoup); + grouped_denominations = json_array (); + GNUNET_assert (NULL != grouped_denominations); + GNUNET_CONTAINER_multipeermap_iterate (ksh->signkey_map, &add_sign_key_cb, &sctx); - recoup = json_array (); - GNUNET_assert (NULL != recoup); + if (0 == json_array_size (sctx.signkeys)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No online signing keys available. Refusing to generate /keys response.\n"); + ret = GNUNET_NO; + goto CLEANUP; + } heap = GNUNET_CONTAINER_heap_create (GNUNET_CONTAINER_HEAP_ORDER_MAX); { struct DenomKeyCtx dkc = { @@ -1832,72 +2891,176 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) = GNUNET_TIME_relative_min (dkc.min_dk_frequency, sctx.min_sk_frequency); } - denoms = json_array (); - GNUNET_assert (NULL != denoms); - // If age restriction is enabled, initialize the array of age restricted denoms. - if (TEH_extension_enabled (TALER_Extension_AgeRestriction)) - { - age_restricted_denoms = json_array (); - GNUNET_assert (NULL != age_restricted_denoms); - } + last_cherry_pick_date = GNUNET_TIME_UNIT_ZERO_TS; - last_cpd = GNUNET_TIME_UNIT_ZERO_TS; - hash_context = GNUNET_CRYPTO_hash_context_start (); { struct TEH_DenominationKey *dk; + struct GNUNET_CONTAINER_MultiHashMap *denominations_by_group; - /* heap = min heap, sorted by start time */ + denominations_by_group = + GNUNET_CONTAINER_multihashmap_create (1024, + GNUNET_NO /* NO, because keys are only on the stack */); + /* heap = max heap, sorted by start time */ while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap))) { - if (GNUNET_TIME_timestamp_cmp (last_cpd, + if (GNUNET_TIME_timestamp_cmp (last_cherry_pick_date, !=, dk->meta.start) && - (! GNUNET_TIME_absolute_is_zero (last_cpd.abs_time)) ) + (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time)) ) { + /* + * This is not the first entry in the heap (because last_cherry_pick_date != + * GNUNET_TIME_UNIT_ZERO_TS) and the previous entry had a different + * start time. Therefore, we create a new entry in ksh. + */ struct GNUNET_HashCode hc; - GNUNET_CRYPTO_hash_context_finish ( - GNUNET_CRYPTO_hash_context_copy (hash_context), - &hc); + compute_msig_hash (&sig_ctx, + &hc); if (GNUNET_OK != create_krd (ksh, &hc, - last_cpd, + last_cherry_pick_date, sctx.signkeys, recoup, - denoms, - age_restricted_denoms)) + grouped_denominations)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to generate key response data for %s\n", - GNUNET_TIME_timestamp2s (last_cpd)); - GNUNET_CRYPTO_hash_context_abort (hash_context); + GNUNET_TIME_timestamp2s (last_cherry_pick_date)); /* drain heap before destroying it */ while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap))) /* intentionally empty */; GNUNET_CONTAINER_heap_destroy (heap); - json_decref (denoms); - if (NULL != age_restricted_denoms) - json_decref (age_restricted_denoms); - json_decref (sctx.signkeys); - json_decref (recoup); - return GNUNET_SYSERR; + goto CLEANUP; } } - last_cpd = dk->meta.start; - GNUNET_CRYPTO_hash_context_read (hash_context, - &dk->h_denom_pub, - sizeof (struct GNUNET_HashCode)); + last_cherry_pick_date = dk->meta.start; + /* + * Group the denominations by {cipher, value, fees, age_mask}. + * + * For each group we save the group meta-data and the list of + * denominations in this group as a json-blob in the multihashmap + * denominations_by_group. + */ { - json_t *denom; - json_t *array; + struct GroupData *group; + json_t *entry; + struct GNUNET_HashCode key; + struct TALER_DenominationGroup meta = { + .cipher = dk->denom_pub.bsign_pub_key->cipher, + .value = dk->meta.value, + .fees = dk->meta.fees, + .age_mask = dk->meta.age_mask, + }; + + /* Search the group/JSON-blob for the key */ + TALER_denomination_group_get_key (&meta, + &key); + group = GNUNET_CONTAINER_multihashmap_get ( + denominations_by_group, + &key); + if (NULL == group) + { + /* There is no group for this meta-data yet, so we create a new group */ + bool age_restricted = meta.age_mask.bits != 0; + const char *cipher; + + group = GNUNET_new (struct GroupData); + switch (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); + } + /* Create a new array for the denominations in this group */ + group->list = json_array (); + GNUNET_assert (NULL != group->list); + group->json = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + cipher), + GNUNET_JSON_pack_array_steal ("denoms", + group->list), + TALER_JSON_PACK_DENOM_FEES ("fee", + &meta.fees), + TALER_JSON_pack_amount ("value", + &meta.value)); + GNUNET_assert (NULL != group->json); + if (age_restricted) + { + GNUNET_assert ( + 0 == + json_object_set_new (group->json, + "age_mask", + json_integer ( + meta.age_mask.bits))); + /* Remember that we have found at least _one_ age restricted denomination */ + has_age_restricted_denomination = true; + } + group->group_off + = json_array_size (grouped_denominations); + GNUNET_assert (0 == + json_array_append_new ( + grouped_denominations, + group->json)); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (denominations_by_group, + &key, + group, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } - denom = - GNUNET_JSON_PACK ( + /* Now that we have found/created the right group, add the + denomination to the list */ + { + struct HelperDenomination *hd; + struct GNUNET_JSON_PackSpec key_spec; + bool private_key_lost; + + hd = GNUNET_CONTAINER_multihashmap_get (ksh->helpers->denom_keys, + &dk->h_denom_pub.hash); + private_key_lost + = (NULL == hd) || + GNUNET_TIME_absolute_is_past ( + GNUNET_TIME_absolute_add ( + hd->start_time.abs_time, + hd->validity_duration)); + switch (meta.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + key_spec = + GNUNET_JSON_pack_rsa_public_key ( + "rsa_pub", + dk->denom_pub.bsign_pub_key->details.rsa_public_key); + break; + case GNUNET_CRYPTO_BSA_CS: + key_spec = + GNUNET_JSON_pack_data_varsize ( + "cs_pub", + &dk->denom_pub.bsign_pub_key->details.cs_public_key, + sizeof (dk->denom_pub.bsign_pub_key->details.cs_public_key)); + break; + default: + GNUNET_assert (false); + } + + entry = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("master_sig", &dk->master_sig), + GNUNET_JSON_pack_allow_null ( + private_key_lost + ? GNUNET_JSON_pack_bool ("lost", + true) + : GNUNET_JSON_pack_string ("dummy", + NULL)), GNUNET_JSON_pack_timestamp ("stamp_start", dk->meta.start), GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", @@ -1906,79 +3069,162 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) dk->meta.expire_deposit), GNUNET_JSON_pack_timestamp ("stamp_expire_legal", dk->meta.expire_legal), - TALER_JSON_pack_denom_pub ("denom_pub", - &dk->denom_pub), - TALER_JSON_pack_amount ("value", - &dk->meta.value), - TALER_JSON_pack_amount ("fee_withdraw", - &dk->meta.fee_withdraw), - TALER_JSON_pack_amount ("fee_deposit", - &dk->meta.fee_deposit), - TALER_JSON_pack_amount ("fee_refresh", - &dk->meta.fee_refresh), - TALER_JSON_pack_amount ("fee_refund", - &dk->meta.fee_refund)); - - /* Put the denom into the correct array - denoms or age_restricted_denoms - - * depending on the settings and the properties of the denomination */ - if (NULL != age_restricted_denoms && - 0 != dk->meta.age_restrictions.mask) - { - array = age_restricted_denoms; - } - else - { - array = denoms; + key_spec + ); + GNUNET_assert (NULL != entry); } - GNUNET_assert ( - 0 == - json_array_append_new ( - array, - denom)); + /* Build up the running hash of all master signatures of the + denominations */ + append_signature (&sig_ctx, + group->group_off, + (unsigned int) json_array_size (group->list), + &dk->master_sig); + /* Finally, add the denomination to the list of denominations in this + group */ + GNUNET_assert (json_is_array (group->list)); + GNUNET_assert (0 == + json_array_append_new (group->list, + entry)); } - } + } /* loop over heap ends */ + + GNUNET_CONTAINER_multihashmap_iterate (denominations_by_group, + &free_group, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (denominations_by_group); } GNUNET_CONTAINER_heap_destroy (heap); - if (! GNUNET_TIME_absolute_is_zero (last_cpd.abs_time)) + + if (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time)) { struct GNUNET_HashCode hc; - GNUNET_CRYPTO_hash_context_finish (hash_context, - &hc); + compute_msig_hash (&sig_ctx, + &hc); if (GNUNET_OK != create_krd (ksh, &hc, - last_cpd, + last_cherry_pick_date, sctx.signkeys, recoup, - denoms, - age_restricted_denoms)) + grouped_denominations)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to generate key response data for %s\n", - GNUNET_TIME_timestamp2s (last_cpd)); - json_decref (denoms); - if (NULL != age_restricted_denoms) - json_decref (age_restricted_denoms); - json_decref (sctx.signkeys); - json_decref (recoup); - return GNUNET_SYSERR; + GNUNET_TIME_timestamp2s (last_cherry_pick_date)); + goto CLEANUP; } ksh->management_only = false; + + /* Sanity check: Make sure that age restriction is enabled IFF at least + * one age restricted denomination exist */ + if (! has_age_restricted_denomination && TEH_age_restriction_enabled) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Age restriction is enabled, but NO denominations with age restriction found!\n"); + goto CLEANUP; + } + else if (has_age_restricted_denomination && ! TEH_age_restriction_enabled) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Age restriction is NOT enabled, but denominations with age restriction found!\n"); + goto CLEANUP; + } } else { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No denomination keys available. Refusing to generate /keys response.\n"); - GNUNET_CRYPTO_hash_context_abort (hash_context); } - json_decref (sctx.signkeys); + ret = GNUNET_OK; + +CLEANUP: + GNUNET_array_grow (sig_ctx.elements, + sig_ctx.elements_size, + 0); + json_decref (grouped_denominations); + if (NULL != sctx.signkeys) + json_decref (sctx.signkeys); json_decref (recoup); - json_decref (denoms); - if (NULL != age_restricted_denoms) - json_decref (age_restricted_denoms); - return GNUNET_OK; + return ret; +} + + +/** + * Called with information about global fees. + * + * @param cls `struct TEH_KeyStateHandle *` we are building + * @param fees the global fees we charge + * @param purse_timeout when do purses time out + * @param history_expiration how long are account histories preserved + * @param purse_account_limit how many purses are free per account + * @param start_date from when are these fees valid (start date) + * @param end_date until when are these fees valid (end date, exclusive) + * @param master_sig master key signature affirming that this is the correct + * fee (of purpose #TALER_SIGNATURE_MASTER_GLOBAL_FEES) + */ +static void +global_fee_info_cb ( + void *cls, + const struct TALER_GlobalFeeSet *fees, + struct GNUNET_TIME_Relative purse_timeout, + struct GNUNET_TIME_Relative history_expiration, + uint32_t purse_account_limit, + struct GNUNET_TIME_Timestamp start_date, + struct GNUNET_TIME_Timestamp end_date, + const struct TALER_MasterSignatureP *master_sig) +{ + struct TEH_KeyStateHandle *ksh = cls; + struct TEH_GlobalFee *gf; + + if (GNUNET_OK != + TALER_exchange_offline_global_fee_verify ( + start_date, + end_date, + fees, + purse_timeout, + history_expiration, + purse_account_limit, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has global fee with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found global fees with %u purses\n", + purse_account_limit); + gf = GNUNET_new (struct TEH_GlobalFee); + gf->start_date = start_date; + gf->end_date = end_date; + gf->fees = *fees; + gf->purse_timeout = purse_timeout; + gf->history_expiration = history_expiration; + gf->purse_account_limit = purse_account_limit; + gf->master_sig = *master_sig; + GNUNET_CONTAINER_DLL_insert (ksh->gf_head, + ksh->gf_tail, + gf); + GNUNET_assert ( + 0 == + json_array_append_new ( + ksh->global_fees, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("start_date", + start_date), + GNUNET_JSON_pack_timestamp ("end_date", + end_date), + TALER_JSON_PACK_GLOBAL_FEES (fees), + GNUNET_JSON_pack_time_rel ("history_expiration", + history_expiration), + GNUNET_JSON_pack_time_rel ("purse_timeout", + purse_timeout), + GNUNET_JSON_pack_uint64 ("purse_account_limit", + purse_account_limit), + GNUNET_JSON_pack_data_auto ("master_sig", + master_sig)))); } @@ -2019,14 +3265,31 @@ build_key_state (struct HelperState *hs, ksh->helpers = hs; } ksh->denomkey_map = GNUNET_CONTAINER_multihashmap_create (1024, - GNUNET_YES); + true); ksh->signkey_map = GNUNET_CONTAINER_multipeermap_create (32, - GNUNET_NO /* MUST be NO! */); + false /* MUST be false! */); ksh->auditors = json_array (); GNUNET_assert (NULL != ksh->auditors); /* NOTE: fetches master-signed signkeys, but ALSO those that were revoked! */ GNUNET_break (GNUNET_OK == TEH_plugin->preflight (TEH_plugin->cls)); + if (NULL != ksh->global_fees) + json_decref (ksh->global_fees); + ksh->global_fees = json_array (); + qs = TEH_plugin->get_global_fees (TEH_plugin->cls, + &global_fee_info_cb, + ksh); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Loading global fees from DB: %d\n", + qs); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + destroy_key_state (ksh, + true); + return NULL; + } qs = TEH_plugin->iterate_denominations (TEH_plugin->cls, &denomination_info_cb, ksh); @@ -2069,20 +3332,23 @@ build_key_state (struct HelperState *hs, true); return NULL; } + if (management_only) { ksh->management_only = true; return ksh; } + if (GNUNET_OK != finish_keys_response (ksh)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Could not finish /keys response (likely no signing keys available yet)\n"); + "Could not finish /keys response (required data not configured yet)\n"); destroy_key_state (ksh, true); return NULL; } + return ksh; } @@ -2104,16 +3370,8 @@ TEH_keys_update_states () } -/** - * Obtain the key state for the current thread. Should ONLY be used - * directly if @a management_only is true. Otherwise use #TEH_keys_get_state(). - * - * @param management_only if we should NOT run 'finish_keys_response()' - * because we only need the state for the /management/keys API - * @return NULL on error - */ static struct TEH_KeyStateHandle * -get_key_state (bool management_only) +keys_get_state (bool management_only) { struct TEH_KeyStateHandle *old_ksh; struct TEH_KeyStateHandle *ksh; @@ -2131,7 +3389,7 @@ get_key_state (bool management_only) if ( (old_ksh->key_generation < key_generation) || (GNUNET_TIME_absolute_is_past (old_ksh->signature_expires.abs_time)) ) { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Rebuilding /keys, generation upgrade from %llu to %llu\n", (unsigned long long) old_ksh->key_generation, (unsigned long long) key_generation); @@ -2149,27 +3407,58 @@ get_key_state (bool management_only) struct TEH_KeyStateHandle * +TEH_keys_get_state_for_management_only (void) +{ + return keys_get_state (true); +} + + +struct TEH_KeyStateHandle * TEH_keys_get_state (void) { struct TEH_KeyStateHandle *ksh; - ksh = get_key_state (false); + ksh = keys_get_state (false); if (NULL == ksh) return NULL; + if (ksh->management_only) { if (GNUNET_OK != finish_keys_response (ksh)) return NULL; } + return ksh; } +const struct TEH_GlobalFee * +TEH_keys_global_fee_by_time ( + struct TEH_KeyStateHandle *ksh, + struct GNUNET_TIME_Timestamp ts) +{ + for (const struct TEH_GlobalFee *gf = ksh->gf_head; + NULL != gf; + gf = gf->next) + { + if (GNUNET_TIME_timestamp_cmp (ts, + >=, + gf->start_date) && + GNUNET_TIME_timestamp_cmp (ts, + <, + gf->end_date)) + return gf; + } + return NULL; +} + + struct TEH_DenominationKey * -TEH_keys_denomination_by_hash (const struct TALER_DenominationHash *h_denom_pub, - struct MHD_Connection *conn, - MHD_RESULT *mret) +TEH_keys_denomination_by_hash ( + const struct TALER_DenominationHashP *h_denom_pub, + struct MHD_Connection *conn, + MHD_RESULT *mret) { struct TEH_KeyStateHandle *ksh; @@ -2182,17 +3471,18 @@ TEH_keys_denomination_by_hash (const struct TALER_DenominationHash *h_denom_pub, NULL); return NULL; } - return TEH_keys_denomination_by_hash2 (ksh, - h_denom_pub, - conn, - mret); + + return TEH_keys_denomination_by_hash_from_state (ksh, + h_denom_pub, + conn, + mret); } struct TEH_DenominationKey * -TEH_keys_denomination_by_hash2 ( - struct TEH_KeyStateHandle *ksh, - const struct TALER_DenominationHash *h_denom_pub, +TEH_keys_denomination_by_hash_from_state ( + const struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *h_denom_pub, struct MHD_Connection *conn, MHD_RESULT *mret) { @@ -2212,49 +3502,217 @@ TEH_keys_denomination_by_hash2 ( } -struct TALER_BlindedDenominationSignature -TEH_keys_denomination_sign (const struct TALER_DenominationHash *h_denom_pub, - const void *msg, - size_t msg_size, - enum TALER_ErrorCode *ec) +enum TALER_ErrorCode +TEH_keys_denomination_batch_sign ( + unsigned int csds_length, + const struct TEH_CoinSignData csds[static csds_length], + bool for_melt, + struct TALER_BlindedDenominationSignature bss[static csds_length]) +{ + struct TEH_KeyStateHandle *ksh; + struct HelperDenomination *hd; + struct TALER_CRYPTO_RsaSignRequest rsrs[csds_length]; + struct TALER_CRYPTO_CsSignRequest csrs[csds_length]; + struct TALER_BlindedDenominationSignature rs[csds_length]; + struct TALER_BlindedDenominationSignature cs[csds_length]; + unsigned int rsrs_pos = 0; + unsigned int csrs_pos = 0; + enum TALER_ErrorCode ec; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; + for (unsigned int i = 0; i<csds_length; i++) + { + const struct TALER_DenominationHashP *h_denom_pub = csds[i].h_denom_pub; + const struct TALER_BlindedPlanchet *bp = csds[i].bp; + + hd = GNUNET_CONTAINER_multihashmap_get (ksh->helpers->denom_keys, + &h_denom_pub->hash); + if (NULL == hd) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + if (bp->blinded_message->cipher != + hd->denom_pub.bsign_pub_key->cipher) + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + switch (hd->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + rsrs[rsrs_pos].h_rsa = &hd->h_details.h_rsa; + rsrs[rsrs_pos].msg + = bp->blinded_message->details.rsa_blinded_message.blinded_msg; + rsrs[rsrs_pos].msg_size + = bp->blinded_message->details.rsa_blinded_message.blinded_msg_size; + rsrs_pos++; + break; + case GNUNET_CRYPTO_BSA_CS: + csrs[csrs_pos].h_cs = &hd->h_details.h_cs; + csrs[csrs_pos].blinded_planchet + = &bp->blinded_message->details.cs_blinded_message; + csrs_pos++; + break; + default: + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + } + + if ( (0 != csrs_pos) && + (0 != rsrs_pos) ) + { + memset (rs, + 0, + sizeof (rs)); + memset (cs, + 0, + sizeof (cs)); + } + ec = TALER_EC_NONE; + if (0 != csrs_pos) + { + ec = TALER_CRYPTO_helper_cs_batch_sign ( + ksh->helpers->csdh, + csrs_pos, + csrs, + for_melt, + (0 == rsrs_pos) ? bss : cs); + if (TALER_EC_NONE != ec) + { + for (unsigned int i = 0; i<csrs_pos; i++) + TALER_blinded_denom_sig_free (&cs[i]); + return ec; + } + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_CS] += csrs_pos; + } + if (0 != rsrs_pos) + { + ec = TALER_CRYPTO_helper_rsa_batch_sign ( + ksh->helpers->rsadh, + rsrs_pos, + rsrs, + (0 == csrs_pos) ? bss : rs); + if (TALER_EC_NONE != ec) + { + for (unsigned int i = 0; i<csrs_pos; i++) + TALER_blinded_denom_sig_free (&cs[i]); + for (unsigned int i = 0; i<rsrs_pos; i++) + TALER_blinded_denom_sig_free (&rs[i]); + return ec; + } + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA] += rsrs_pos; + } + + if ( (0 != csrs_pos) && + (0 != rsrs_pos) ) + { + rsrs_pos = 0; + csrs_pos = 0; + for (unsigned int i = 0; i<csds_length; i++) + { + const struct TALER_BlindedPlanchet *bp = csds[i].bp; + + switch (bp->blinded_message->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + bss[i] = rs[rsrs_pos++]; + break; + case GNUNET_CRYPTO_BSA_CS: + bss[i] = cs[csrs_pos++]; + break; + default: + GNUNET_assert (0); + } + } + } + return TALER_EC_NONE; +} + + +enum TALER_ErrorCode +TEH_keys_denomination_cs_r_pub ( + const struct TEH_CsDeriveData *cdd, + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP *r_pub) { + const struct TALER_DenominationHashP *h_denom_pub = cdd->h_denom_pub; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdd->nonce; struct TEH_KeyStateHandle *ksh; - struct TALER_BlindedDenominationSignature none; struct HelperDenomination *hd; - memset (&none, - 0, - sizeof (none)); ksh = TEH_keys_get_state (); if (NULL == ksh) { - *ec = TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; - return none; + return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; } hd = GNUNET_CONTAINER_multihashmap_get (ksh->helpers->denom_keys, &h_denom_pub->hash); if (NULL == hd) { - *ec = TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; - return none; + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + } + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) + { + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + + { + struct TALER_CRYPTO_CsDeriveRequest cdr = { + .h_cs = &hd->h_details.h_cs, + .nonce = nonce + }; + return TALER_CRYPTO_helper_cs_r_derive (ksh->helpers->csdh, + &cdr, + for_melt, + r_pub); + } +} + + +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub ( + unsigned int cdds_length, + const struct TEH_CsDeriveData cdds[static cdds_length], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]) +{ + struct TEH_KeyStateHandle *ksh; + struct HelperDenomination *hd; + struct TALER_CRYPTO_CsDeriveRequest cdrs[cdds_length]; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; } - switch (hd->denom_pub.cipher) + for (unsigned int i = 0; i<cdds_length; i++) { - case TALER_DENOMINATION_RSA: - return TALER_CRYPTO_helper_rsa_sign (ksh->helpers->dh, - &hd->h_details.h_rsa, - msg, - msg_size, - ec); - default: - *ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - return none; + const struct TALER_DenominationHashP *h_denom_pub = cdds[i].h_denom_pub; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdds[i].nonce; + + hd = GNUNET_CONTAINER_multihashmap_get (ksh->helpers->denom_keys, + &h_denom_pub->hash); + if (NULL == hd) + { + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + } + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) + { + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + cdrs[i].h_cs = &hd->h_details.h_cs; + cdrs[i].nonce = nonce; } + + return TALER_CRYPTO_helper_cs_r_batch_derive (ksh->helpers->csdh, + cdds_length, + cdrs, + for_melt, + r_pubs); } void -TEH_keys_denomination_revoke (const struct TALER_DenominationHash *h_denom_pub) +TEH_keys_denomination_revoke (const struct TALER_DenominationHashP *h_denom_pub) { struct TEH_KeyStateHandle *ksh; struct HelperDenomination *hd; @@ -2272,17 +3730,23 @@ TEH_keys_denomination_revoke (const struct TALER_DenominationHash *h_denom_pub) GNUNET_break (0); return; } - switch (hd->denom_pub.cipher) + switch (hd->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: - TALER_CRYPTO_helper_rsa_revoke (ksh->helpers->dh, + case GNUNET_CRYPTO_BSA_INVALID: + break; + case GNUNET_CRYPTO_BSA_RSA: + TALER_CRYPTO_helper_rsa_revoke (ksh->helpers->rsadh, &hd->h_details.h_rsa); TEH_keys_update_states (); return; - default: - GNUNET_break (0); + case GNUNET_CRYPTO_BSA_CS: + TALER_CRYPTO_helper_cs_revoke (ksh->helpers->csdh, + &hd->h_details.h_cs); + TEH_keys_update_states (); return; } + GNUNET_break (0); + return; } @@ -2312,13 +3776,15 @@ TEH_keys_exchange_sign_ ( enum TALER_ErrorCode TEH_keys_exchange_sign2_ ( - struct TEH_KeyStateHandle *ksh, + void *cls, const struct GNUNET_CRYPTO_EccSignaturePurpose *purpose, struct TALER_ExchangePublicKeyP *pub, struct TALER_ExchangeSignatureP *sig) { + struct TEH_KeyStateHandle *ksh = cls; enum TALER_ErrorCode ec; + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_EDDSA]++; ec = TALER_CRYPTO_helper_esign_sign_ (ksh->helpers->esh, purpose, pub, @@ -2401,7 +3867,11 @@ TEH_keys_get_handler (struct TEH_RequestContext *rc, const char *const args[]) { struct GNUNET_TIME_Timestamp last_issue_date; + const char *etag; + etag = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); (void) args; { const char *have_cherrypick; @@ -2441,7 +3911,8 @@ TEH_keys_get_handler (struct TEH_RequestContext *rc, const struct KeysResponseData *krd; ksh = TEH_keys_get_state (); - if (NULL == ksh) + if ( (NULL == ksh) || + (0 == ksh->krd_array_length) ) { if ( ( (SKR_LIMIT == skr_size) && (rc->connection == skr_connection) ) || @@ -2482,6 +3953,14 @@ TEH_keys_get_handler (struct TEH_RequestContext *rc, Wait until they are. */ return suspend_request (rc->connection); } + if ( (NULL != etag) && + (0 == strcmp (etag, + krd->etag)) ) + return TEH_RESPONSE_reply_not_modified (rc->connection, + krd->etag, + &setup_general_response_headers, + ksh); + return MHD_queue_response (rc->connection, MHD_HTTP_OK, (MHD_YES == @@ -2551,92 +4030,35 @@ load_extension_data (const char *section_name, section_name); return GNUNET_SYSERR; } - if (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, - section_name, - "FEE_WITHDRAW", - &meta->fee_withdraw)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "Need amount for option `%s' in section `%s'\n", - "FEE_WITHDRAW", - section_name); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, - section_name, - "FEE_DEPOSIT", - &meta->fee_deposit)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "Need amount for option `%s' in section `%s'\n", - "FEE_DEPOSIT", - section_name); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, - section_name, - "FEE_REFRESH", - &meta->fee_refresh)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "Need amount for option `%s' in section `%s'\n", - "FEE_REFRESH", - section_name); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, - section_name, - "FEE_REFUND", - &meta->fee_refund)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "Need amount for option `%s' in section `%s'\n", - "FEE_REFUND", - section_name); - return GNUNET_SYSERR; - } - if ( (0 != strcasecmp (TEH_currency, - meta->value.currency)) || - (0 != strcasecmp (TEH_currency, - meta->fee_withdraw.currency)) || - (0 != strcasecmp (TEH_currency, - meta->fee_deposit.currency)) || - (0 != strcasecmp (TEH_currency, - meta->fee_refresh.currency)) || - (0 != strcasecmp (TEH_currency, - meta->fee_refund.currency)) ) + if (0 != strcasecmp (TEH_currency, + meta->value.currency)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Need amounts in section `%s' to use currency `%s'\n", + "Need denomination value in section `%s' to use currency `%s'\n", section_name, TEH_currency); return GNUNET_SYSERR; } - meta->age_restrictions = load_age_mask (section_name); + if (GNUNET_OK != + TALER_config_get_denom_fees (TEH_cfg, + TEH_currency, + section_name, + &meta->fees)) + return GNUNET_SYSERR; + meta->age_mask = load_age_mask (section_name); return GNUNET_OK; } enum GNUNET_GenericReturnValue -TEH_keys_load_fees (const struct TALER_DenominationHash *h_denom_pub, +TEH_keys_load_fees (struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *h_denom_pub, struct TALER_DenominationPublicKey *denom_pub, struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta) { - struct TEH_KeyStateHandle *ksh; struct HelperDenomination *hd; enum GNUNET_GenericReturnValue ok; - ksh = get_key_state (true); - if (NULL == ksh) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - hd = GNUNET_CONTAINER_multihashmap_get (ksh->helpers->denom_keys, &h_denom_pub->hash); if (NULL == hd) @@ -2654,9 +4076,10 @@ TEH_keys_load_fees (const struct TALER_DenominationHash *h_denom_pub, meta); if (GNUNET_OK == ok) { - GNUNET_assert (TALER_DENOMINATION_INVALID != hd->denom_pub.cipher); - TALER_denom_pub_deep_copy (denom_pub, - &hd->denom_pub); + GNUNET_assert (GNUNET_CRYPTO_BSA_INVALID != + hd->denom_pub.bsign_pub_key->cipher); + TALER_denom_pub_copy (denom_pub, + &hd->denom_pub); } else { @@ -2679,7 +4102,7 @@ TEH_keys_get_timing (const struct TALER_ExchangePublicKeyP *exchange_pub, struct HelperSignkey *hsk; struct GNUNET_PeerIdentity pid; - ksh = get_key_state (true); + ksh = TEH_keys_get_state_for_management_only (); if (NULL == ksh) { GNUNET_break (0); @@ -2689,6 +4112,11 @@ TEH_keys_get_timing (const struct TALER_ExchangePublicKeyP *exchange_pub, pid.public_key = exchange_pub->eddsa_pub; hsk = GNUNET_CONTAINER_multipeermap_get (ksh->helpers->esign_keys, &pid); + if (NULL == hsk) + { + GNUNET_break (0); + return GNUNET_NO; + } meta->start = hsk->start_time; meta->expire_sign = GNUNET_TIME_absolute_to_timestamp ( @@ -2743,7 +4171,7 @@ add_future_denomkey_cb (void *cls, struct FutureBuilderContext *fbc = cls; struct HelperDenomination *hd = value; struct TEH_DenominationKey *dk; - struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; + struct TALER_EXCHANGEDB_DenominationKeyMetaData meta = {0}; dk = GNUNET_CONTAINER_multihashmap_get (fbc->ksh->denomkey_map, h_denom_pub); @@ -2779,14 +4207,8 @@ add_future_denomkey_cb (void *cls, meta.expire_legal), TALER_JSON_pack_denom_pub ("denom_pub", &hd->denom_pub), - TALER_JSON_pack_amount ("fee_withdraw", - &meta.fee_withdraw), - TALER_JSON_pack_amount ("fee_deposit", - &meta.fee_deposit), - TALER_JSON_pack_amount ("fee_refresh", - &meta.fee_refresh), - TALER_JSON_pack_amount ("fee_refund", - &meta.fee_refund), + TALER_JSON_PACK_DENOM_FEES ("fee", + &meta.fees), GNUNET_JSON_pack_data_auto ("denom_secmod_sig", &hd->sm_sig), GNUNET_JSON_pack_string ("section_name", @@ -2855,7 +4277,7 @@ TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, json_t *reply; (void) rh; - ksh = get_key_state (true); + ksh = TEH_keys_get_state_for_management_only (); if (NULL == ksh) { return TALER_MHD_reply_with_error (connection, @@ -2872,8 +4294,10 @@ TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, .signkeys = json_array () }; - if (GNUNET_is_zero (&denom_sm_pub)) + if ( (GNUNET_is_zero (&denom_rsa_sm_pub)) && + (GNUNET_is_zero (&denom_cs_sm_pub)) ) { + /* Either IPC failed, or neither helper had any denominations configured. */ return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_GATEWAY, TALER_EC_EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE, @@ -2886,7 +4310,6 @@ TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, TALER_EC_EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE, NULL); } - // then a secmod helper is not yet running and we should return an MHD_HTTP_BAD_GATEWAY! GNUNET_assert (NULL != fbc.denoms); GNUNET_assert (NULL != fbc.signkeys); GNUNET_CONTAINER_multihashmap_iterate (ksh->helpers->denom_keys, @@ -2903,7 +4326,9 @@ TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, GNUNET_JSON_pack_data_auto ("master_pub", &TEH_master_public_key), GNUNET_JSON_pack_data_auto ("denom_secmod_public_key", - &denom_sm_pub), + &denom_rsa_sm_pub), + GNUNET_JSON_pack_data_auto ("denom_secmod_cs_public_key", + &denom_cs_sm_pub), GNUNET_JSON_pack_data_auto ("signkey_secmod_public_key", &esign_sm_pub)); GNUNET_log (GNUNET_ERROR_TYPE_INFO, diff --git a/src/exchange/taler-exchange-httpd_keys.h b/src/exchange/taler-exchange-httpd_keys.h index ce9068ec2..e526385ff 100644 --- a/src/exchange/taler-exchange-httpd_keys.h +++ b/src/exchange/taler-exchange-httpd_keys.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020 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 Affero General Public License as published by the Free Software @@ -49,7 +49,7 @@ struct TEH_DenominationKey /** * Hash code of the denomination public key. */ - struct TALER_DenominationHash h_denom_pub; + struct TALER_DenominationHashP h_denom_pub; /** * Meta data about the type of the denomination, such as fees and validity @@ -83,6 +83,59 @@ struct TEH_DenominationKey /** + * Set of global fees (and options) for a time range. + */ +struct TEH_GlobalFee +{ + /** + * Kept in a DLL. + */ + struct TEH_GlobalFee *next; + + /** + * Kept in a DLL. + */ + struct TEH_GlobalFee *prev; + + /** + * Beginning of the validity period (inclusive). + */ + struct GNUNET_TIME_Timestamp start_date; + + /** + * End of the validity period (exclusive). + */ + struct GNUNET_TIME_Timestamp end_date; + + /** + * How long do unmerged purses stay around at most? + */ + struct GNUNET_TIME_Relative purse_timeout; + + /** + * What is the longest history we return? + */ + struct GNUNET_TIME_Relative history_expiration; + + /** + * Signature affirming these details. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Fee structure for operations that do not depend + * on a denomination or wire method. + */ + struct TALER_GlobalFeeSet fees; + + /** + * Number of free purses per account. + */ + uint32_t purse_account_limit; +}; + + +/** * Snapshot of the (coin and signing) keys (including private keys) of * the exchange. There can be multiple instances of this struct, as it is * reference counted and only destroyed once the last user is done @@ -101,6 +154,48 @@ struct TEH_KeyStateHandle; void TEH_check_invariants (void); +/** + * Clean up wire subsystem. + */ +void +TEH_wire_done (void); + + +/** + * Look up wire fee structure by @a ts. + * + * @param ts timestamp to lookup wire fees at + * @param method wire method to lookup fees for + * @return the wire fee details, or + * NULL if none are configured for @a ts and @a method + */ +const struct TALER_WireFeeSet * +TEH_wire_fees_by_time ( + struct GNUNET_TIME_Timestamp ts, + const char *method); + + +/** + * Initialize wire subsystem. + * + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TEH_wire_init (void); + + +/** + * Something changed in the database. Rebuild the wire replies. This function + * should be called if the exchange learns about a new signature from our + * master key. + * + * (We do not do so immediately, but merely signal to all threads that they + * need to rebuild their wire state upon the next call to + * #TEH_keys_get_state()). + */ +void +TEH_wire_update_state (void); + /** * Return the current key state for this thread. Possibly re-builds the key @@ -115,6 +210,12 @@ TEH_check_invariants (void); struct TEH_KeyStateHandle * TEH_keys_get_state (void); +/** + * Obtain the key state if we should NOT run finish_keys_response() because we + * only need the state for the /management/keys API + */ +struct TEH_KeyStateHandle * +TEH_keys_get_state_for_management_only (void); /** * Something changed in the database. Rebuild all key states. This function @@ -130,6 +231,20 @@ TEH_keys_update_states (void); /** + * Look up global fee structure by @a ts. + * + * @param ksh key state state to look in + * @param ts timestamp to lookup global fees at + * @return the global fee details, or + * NULL if none are configured for @a ts + */ +const struct TEH_GlobalFee * +TEH_keys_global_fee_by_time ( + struct TEH_KeyStateHandle *ksh, + struct GNUNET_TIME_Timestamp ts); + + +/** * Look up the issue for a denom public key. Note that the result * must only be used in this thread and only until another key or * key state is resolved. @@ -141,9 +256,10 @@ TEH_keys_update_states (void); * or NULL if @a h_denom_pub could not be found */ struct TEH_DenominationKey * -TEH_keys_denomination_by_hash (const struct TALER_DenominationHash *h_denom_pub, - struct MHD_Connection *conn, - MHD_RESULT *mret); +TEH_keys_denomination_by_hash ( + const struct TALER_DenominationHashP *h_denom_pub, + struct MHD_Connection *conn, + MHD_RESULT *mret); /** @@ -160,32 +276,98 @@ TEH_keys_denomination_by_hash (const struct TALER_DenominationHash *h_denom_pub, * or NULL if @a h_denom_pub could not be found */ struct TEH_DenominationKey * -TEH_keys_denomination_by_hash2 (struct TEH_KeyStateHandle *ksh, - const struct - TALER_DenominationHash *h_denom_pub, - struct MHD_Connection *conn, - MHD_RESULT *mret); +TEH_keys_denomination_by_hash_from_state ( + const struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *h_denom_pub, + struct MHD_Connection *conn, + MHD_RESULT *mret); /** - * Request to sign @a msg using the public key corresponding to - * @a h_denom_pub. + * Information needed to create a blind signature. + */ +struct TEH_CoinSignData +{ + /** + * Hash of key to sign with. + */ + const struct TALER_DenominationHashP *h_denom_pub; + + /** + * Blinded planchet to sign over. + */ + const struct TALER_BlindedPlanchet *bp; +}; + + +/** + * Request to sign @a csds. * - * @param h_denom_pub hash of the public key to use to sign - * @param msg message to sign - * @param msg_size number of bytes in @a msg - * @param[out] ec set to the error code (or #TALER_EC_NONE on success) - * @return signature, the value inside the structure will be NULL on failure, - * see @a ec for details about the failure + * @param csds array with data to blindly sign (and keys to sign with) + * @param csds_length length of @a csds array + * @param for_melt true if this is for a melt operation + * @param[out] bss array set to the blind signature on success; must be of length @a csds_length + * @return #TALER_EC_NONE on success */ -struct TALER_BlindedDenominationSignature -TEH_keys_denomination_sign (const struct TALER_DenominationHash *h_denom_pub, - const void *msg, - size_t msg_size, - enum TALER_ErrorCode *ec); +enum TALER_ErrorCode +TEH_keys_denomination_batch_sign ( + unsigned int csds_length, + const struct TEH_CoinSignData csds[static csds_length], + bool for_melt, + struct TALER_BlindedDenominationSignature bss[static csds_length]); /** - * Revoke the public key associated with @param h_denom_pub . + * Information needed to derive the CS r_pub. + */ +struct TEH_CsDeriveData +{ + /** + * Hash of key to sign with. + */ + const struct TALER_DenominationHashP *h_denom_pub; + + /** + * Nonce to use. + */ + const struct GNUNET_CRYPTO_CsSessionNonce *nonce; +}; + + +/** + * Request to derive CS @a r_pub using the denomination and nonce from @a cdd. + * + * @param cdd data to compute @a r_pub from + * @param for_melt true if this is for a melt operation + * @param[out] r_pub where to write the result + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_denomination_cs_r_pub ( + const struct TEH_CsDeriveData *cdd, + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP *r_pub); + + +/** + * Request to derive a bunch of CS @a r_pubs using the + * denominations and nonces from @a cdds. + * + * @param cdds array to compute @a r_pubs from + * @param cdds_length length of the @a cdds array + * @param for_melt true if this is for a melt operation + * @param[out] r_pubs array where to write the result; must be of length @a cdds_length + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub ( + unsigned int cdds_length, + const struct TEH_CsDeriveData cdds[static cdds_length], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]); + + +/** + * Revoke the public key associated with @a h_denom_pub. * This function should be called AFTER the database was * updated, as it also triggers #TEH_keys_update_states(). * @@ -196,7 +378,8 @@ TEH_keys_denomination_sign (const struct TALER_DenominationHash *h_denom_pub, * @param h_denom_pub hash of the public key to revoke */ void -TEH_keys_denomination_revoke (const struct TALER_DenominationHash *h_denom_pub); +TEH_keys_denomination_revoke ( + const struct TALER_DenominationHashP *h_denom_pub); /** @@ -207,7 +390,7 @@ TEH_keys_finished (void); /** - * Resumse all suspended /keys requests, we may now have key material + * Resumes all suspended /keys requests, we may now have key material * (or are shutting down). * * @param do_shutdown are we shutting down? @@ -244,7 +427,7 @@ TEH_keys_exchange_sign_ ( * number of bytes of the data structure, including its header. Use * #TEH_keys_exchange_sign() instead of calling this function directly! * - * @param ksh key state state to look in + * @param cls key state state to look in * @param purpose the message to sign * @param[out] pub set to the current public signing key of the exchange * @param[out] sig signature over purpose using current signing key @@ -252,7 +435,7 @@ TEH_keys_exchange_sign_ ( */ enum TALER_ErrorCode TEH_keys_exchange_sign2_ ( - struct TEH_KeyStateHandle *ksh, + void *cls, const struct GNUNET_CRYPTO_EccSignaturePurpose *purpose, struct TALER_ExchangePublicKeyP *pub, struct TALER_ExchangeSignatureP *sig); @@ -364,6 +547,7 @@ TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, * Load fees and expiration times (!) for the denomination type configured for * the denomination matching @a h_denom_pub. * + * @param ksh key state to load fees from * @param h_denom_pub hash of the denomination public key * to use to derive the section name of the configuration to use * @param[out] denom_pub set to the denomination public key (to be freed by caller!) @@ -373,7 +557,8 @@ TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, * #GNUNET_SYSERR on hard errors */ enum GNUNET_GenericReturnValue -TEH_keys_load_fees (const struct TALER_DenominationHash *h_denom_pub, +TEH_keys_load_fees (struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *h_denom_pub, struct TALER_DenominationPublicKey *denom_pub, struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta); diff --git a/src/exchange/taler-exchange-httpd_kyc-check.c b/src/exchange/taler-exchange-httpd_kyc-check.c index 7560d6262..362c20a2e 100644 --- a/src/exchange/taler-exchange-httpd_kyc-check.c +++ b/src/exchange/taler-exchange-httpd_kyc-check.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021 Taler Systems SA + 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 Affero General Public License as published by the Free Software @@ -25,6 +25,7 @@ #include <microhttpd.h> #include <pthread.h> #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler_signatures.h" #include "taler_dbevents.h" @@ -54,42 +55,93 @@ struct KycPoller struct MHD_Connection *connection; /** + * Logic for @e ih + */ + struct TALER_KYCLOGIC_Plugin *ih_logic; + + /** + * Handle to asynchronously running KYC initiation + * request. + */ + struct TALER_KYCLOGIC_InitiateHandle *ih; + + /** * Subscription for the database event we are * waiting for. */ struct GNUNET_DB_EventHandler *eh; /** - * UUID being checked. + * Row of the requirement being checked. */ - uint64_t payment_target_uuid; + uint64_t requirement_row; /** - * Current KYC status. + * Row of KYC process being initiated. */ - struct TALER_EXCHANGEDB_KycStatus kyc; + uint64_t process_row; /** * Hash of the payto:// URI we are confirming to * have finished the KYC for. */ - struct TALER_PaytoHash h_payto; + struct TALER_PaytoHashP h_payto; /** - * Hash of the payto:// URI that was given to us for auth. + * When will this request time out? */ - struct TALER_PaytoHash auth_h_payto; + struct GNUNET_TIME_Absolute timeout; /** - * When will this request time out? + * If the KYC complete, what kind of data was collected? */ - struct GNUNET_TIME_Absolute timeout; + json_t *kyc_details; + + /** + * Set to starting URL of KYC process if KYC is required. + */ + char *kyc_url; + + /** + * Set to error details, on error (@ec not TALER_EC_NONE). + */ + char *hint; + + /** + * Name of the section of the provider in the configuration. + */ + const char *section_name; + + /** + * Set to AML status of the account. + */ + enum TALER_AmlDecisionState aml_status; + + /** + * Set to error encountered with KYC logic, if any. + */ + enum TALER_ErrorCode ec; + + /** + * What kind of entity is doing the KYC check? + */ + enum TALER_KYCLOGIC_KycUserType ut; /** * True if we are still suspended. */ bool suspended; + /** + * False if KYC is not required. + */ + bool kyc_required; + + /** + * True if we once tried the KYC initiation. + */ + bool ih_done; + }; @@ -114,6 +166,11 @@ TEH_kyc_check_cleanup () GNUNET_CONTAINER_DLL_remove (kyp_head, kyp_tail, kyp); + if (NULL != kyp->ih) + { + kyp->ih_logic->initiate_cancel (kyp->ih); + kyp->ih = NULL; + } if (kyp->suspended) { kyp->suspended = false; @@ -143,11 +200,86 @@ kyp_cleanup (struct TEH_RequestContext *rc) kyp->eh); kyp->eh = NULL; } + if (NULL != kyp->ih) + { + kyp->ih_logic->initiate_cancel (kyp->ih); + kyp->ih = NULL; + } + json_decref (kyp->kyc_details); + GNUNET_free (kyp->kyc_url); + GNUNET_free (kyp->hint); GNUNET_free (kyp); } /** + * Function called with the result of a KYC initiation + * operation. + * + * @param cls closure with our `struct KycPoller *` + * @param ec #TALER_EC_NONE on success + * @param redirect_url set to where to redirect the user on success, NULL on failure + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param error_msg_hint set to additional details to return to user, NULL on success + */ +static void +initiate_cb ( + void *cls, + enum TALER_ErrorCode ec, + const char *redirect_url, + const char *provider_user_id, + const char *provider_legitimization_id, + const char *error_msg_hint) +{ + struct KycPoller *kyp = cls; + enum GNUNET_DB_QueryStatus qs; + + kyp->ih = NULL; + kyp->ih_done = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC initiation `%s' completed with ec=%d (%s)\n", + provider_legitimization_id, + ec, + (TALER_EC_NONE == ec) + ? redirect_url + : error_msg_hint); + kyp->ec = ec; + if (TALER_EC_NONE == ec) + { + kyp->kyc_url = GNUNET_strdup (redirect_url); + } + else + { + kyp->hint = GNUNET_strdup (error_msg_hint); + } + qs = TEH_plugin->update_kyc_process_by_row ( + TEH_plugin->cls, + kyp->process_row, + kyp->section_name, + &kyp->h_payto, + provider_user_id, + provider_legitimization_id, + redirect_url, + GNUNET_TIME_UNIT_ZERO_ABS); + if (qs <= 0) + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC requirement update failed for %s with status %d at %s:%u\n", + TALER_B2S (&kyp->h_payto), + qs, + __FILE__, + __LINE__); + GNUNET_assert (kyp->suspended); + kyp->suspended = false; + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + MHD_resume_connection (kyp->connection); + TALER_MHD_daemon_trigger (); +} + + +/** * Function implementing database transaction to check wallet's KYC status. * Runs the transaction logic; IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns an hard error, the @@ -168,11 +300,53 @@ kyc_check (void *cls, { struct KycPoller *kyp = cls; enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->select_kyc_status (TEH_plugin->cls, - kyp->payment_target_uuid, - &kyp->h_payto, - &kyp->kyc); + struct TALER_KYCLOGIC_ProviderDetails *pd; + enum GNUNET_GenericReturnValue ret; + struct TALER_PaytoHashP h_payto; + char *requirements; + char *redirect_url; + bool satisfied; + + qs = TEH_plugin->lookup_kyc_requirement_by_row ( + TEH_plugin->cls, + kyp->requirement_row, + &requirements, + &kyp->aml_status, + &h_payto); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No KYC requirements open for %llu\n", + (unsigned long long) kyp->requirement_row); + return qs; + } + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + return qs; + } + if (0 != + GNUNET_memcmp (&kyp->h_payto, + &h_payto)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Requirement %llu provided, but h_payto does not match\n", + (unsigned long long) kyp->requirement_row); + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, + "h_payto"); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_HARD_ERROR; + } + qs = TALER_KYCLOGIC_check_satisfied ( + &requirements, + &h_payto, + &kyp->kyc_details, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &satisfied); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -181,9 +355,95 @@ kyc_check (void *cls, *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "inselect_wallet_status"); + "kyc_test_required"); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (satisfied) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC requirements `%s' already satisfied\n", + requirements); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + + kyp->kyc_required = true; + ret = TALER_KYCLOGIC_requirements_to_logic (requirements, + kyp->ut, + &kyp->ih_logic, + &pd, + &kyp->section_name); + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC requirements `%s' cannot be checked, but are set as required in database!\n", + requirements); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_GONE, + requirements); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_free (requirements); + + if (kyp->ih_done) + return qs; + qs = TEH_plugin->get_pending_kyc_requirement_process ( + TEH_plugin->cls, + &h_payto, + kyp->section_name, + &redirect_url); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_process"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if ( (qs > 0) && + (NULL != redirect_url) ) + { + kyp->kyc_url = redirect_url; return qs; } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* set up new requirement process */ + qs = TEH_plugin->insert_kyc_requirement_process ( + TEH_plugin->cls, + &h_payto, + kyp->section_name, + NULL, + NULL, + &kyp->process_row); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_process"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Initiating KYC check with logic %s\n", + kyp->ih_logic->name); + kyp->ih = kyp->ih_logic->initiate (kyp->ih_logic->cls, + pd, + &h_payto, + kyp->process_row, + &initiate_cb, + kyp); + GNUNET_break (NULL != kyp->ih); return qs; } @@ -231,7 +491,7 @@ db_event_cb (void *cls, MHD_RESULT TEH_handler_kyc_check ( struct TEH_RequestContext *rc, - const char *const args[]) + const char *const args[3]) { struct KycPoller *kyp = rc->rh_ctx; MHD_RESULT res; @@ -246,87 +506,62 @@ TEH_handler_kyc_check ( rc->rh_cleaner = &kyp_cleanup; { - unsigned long long payment_target_uuid; + unsigned long long requirement_row; char dummy; if (1 != sscanf (args[0], "%llu%c", - &payment_target_uuid, + &requirement_row, &dummy)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payment_target_uuid"); + "requirement_row"); } - kyp->payment_target_uuid = (uint64_t) payment_target_uuid; + kyp->requirement_row = (uint64_t) requirement_row; } - { - const char *ts; - ts = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, - "timeout_ms"); - if (NULL != ts) - { - char dummy; - unsigned long long tms; - - if (1 != - sscanf (ts, - "%llu%c", - &tms, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "timeout_ms"); - } - kyp->timeout = GNUNET_TIME_relative_to_absolute ( - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, - tms)); - } - } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[1], + strlen (args[1]), + &kyp->h_payto, + sizeof (kyp->h_payto))) { - const char *hps; - - hps = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, "h_payto"); - if (NULL == hps) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "h_payto"); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (hps, - strlen (hps), - &kyp->auth_h_payto, - sizeof (kyp->auth_h_payto))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "h_payto"); - } } - } - if (TEH_KYC_NONE == TEH_kyc_config.mode) - return TALER_MHD_reply_static ( + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_user_type_from_string (args[2], + &kyp->ut)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "usertype"); + } + + TALER_MHD_parse_request_timeout (rc->connection, + &kyp->timeout); + } + /* KYC plugin generated reply? */ + if (NULL != kyp->kyc_url) + { + return TALER_MHD_REPLY_JSON_PACK ( rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status), + GNUNET_JSON_pack_string ("kyc_url", + kyp->kyc_url)); + } if ( (NULL == kyp->eh) && GNUNET_TIME_absolute_is_future (kyp->timeout) ) @@ -334,7 +569,7 @@ TEH_handler_kyc_check ( struct TALER_KycCompletedEventP rep = { .header.size = htons (sizeof (rep)), .header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED), - .h_payto = kyp->auth_h_payto + .h_payto = kyp->h_payto }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -350,29 +585,78 @@ TEH_handler_kyc_check ( now = GNUNET_TIME_timestamp_get (); ret = TEH_DB_run_transaction (rc->connection, "kyc check", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &res, &kyc_check, kyp); if (GNUNET_SYSERR == ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Transaction failed.\n"); return res; - if (0 != - GNUNET_memcmp (&kyp->h_payto, - &kyp->auth_h_payto)) + } + /* KYC plugin generated reply? */ + if (NULL != kyp->kyc_url) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_UNAUTHORIZED, - TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, - "h_payto"); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status), + GNUNET_JSON_pack_string ("kyc_url", + kyp->kyc_url)); + } + + if ( (NULL == kyp->ih) && + (! kyp->kyc_required) ) + { + if (TALER_AML_NORMAL != kyp->aml_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC is OK, but AML active: %d\n", + (int) kyp->aml_status); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status)); + } + /* KYC not required */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC not required %llu\n", + (unsigned long long) kyp->requirement_row); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + + if (NULL != kyp->ih) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending HTTP request on KYC logic...\n"); + kyp->suspended = true; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (kyp->connection); + return MHD_YES; } /* long polling? */ - if ( (! kyp->kyc.ok) && + if ( (NULL != kyp->section_name) && GNUNET_TIME_absolute_is_future (kyp->timeout)) { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending HTTP request on timeout (%s) now...\n", + GNUNET_TIME_relative2s (GNUNET_TIME_absolute_get_remaining ( + kyp->timeout), + true)); GNUNET_assert (NULL != kyp->eh); kyp->suspended = true; + kyp->section_name = NULL; GNUNET_CONTAINER_DLL_insert (kyp_head, kyp_tail, kyp); @@ -380,53 +664,27 @@ TEH_handler_kyc_check ( return MHD_YES; } - /* KYC failed? */ - if (! kyp->kyc.ok) + if (TALER_EC_NONE != kyp->ec) { - char *url; - char *redirect_uri; - char *redirect_uri_encoded; - - GNUNET_assert (TEH_KYC_OAUTH2 == TEH_kyc_config.mode); - GNUNET_asprintf (&redirect_uri, - "%s/kyc-proof/%llu", - TEH_base_url, - (unsigned long long) kyp->payment_target_uuid); - redirect_uri_encoded = TALER_urlencode (redirect_uri); - GNUNET_free (redirect_uri); - GNUNET_asprintf (&url, - "%s/login?client_id=%s&redirect_uri=%s", - TEH_kyc_config.details.oauth2.url, - TEH_kyc_config.details.oauth2.client_id, - redirect_uri_encoded); - GNUNET_free (redirect_uri_encoded); - - res = TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ("kyc_url", - url)); - GNUNET_free (url); - return res; + return TALER_MHD_reply_with_ec (rc->connection, + kyp->ec, + kyp->hint); } - /* KYC succeeded! */ + /* KYC must have succeeded! */ { struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; - struct TALER_ExchangeAccountSetupSuccessPS as = { - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_ACCOUNT_SETUP_SUCCESS), - .purpose.size = htonl (sizeof (as)), - .h_payto = kyp->h_payto, - .timestamp = GNUNET_TIME_timestamp_hton (now) - }; enum TALER_ErrorCode ec; if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign (&as, - &pub, - &sig))) + (ec = TALER_exchange_online_account_setup_success_sign ( + &TEH_keys_exchange_sign_, + &kyp->h_payto, + kyp->kyc_details, + now, + &pub, + &sig))) { return TALER_MHD_reply_with_ec (rc->connection, ec, @@ -439,6 +697,10 @@ TEH_handler_kyc_check ( &sig), GNUNET_JSON_pack_data_auto ("exchange_pub", &pub), + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status), + GNUNET_JSON_pack_object_incref ("kyc_details", + kyp->kyc_details), GNUNET_JSON_pack_timestamp ("now", now)); } diff --git a/src/exchange/taler-exchange-httpd_kyc-check.h b/src/exchange/taler-exchange-httpd_kyc-check.h index 52fe5f16b..f1f2c9e7d 100644 --- a/src/exchange/taler-exchange-httpd_kyc-check.h +++ b/src/exchange/taler-exchange-httpd_kyc-check.h @@ -29,8 +29,8 @@ * Handle a "/kyc-check" request. Checks the KYC * status of the given account and returns it. * - * @param connection request to handle - * @param args one argument with the payment_target_uuid + * @param rc details about the request to handle + * @param args one argument with the legitimization_uuid * @return MHD result code */ MHD_RESULT diff --git a/src/exchange/taler-exchange-httpd_kyc-proof.c b/src/exchange/taler-exchange-httpd_kyc-proof.c index 24ddfc74d..bad377a2a 100644 --- a/src/exchange/taler-exchange-httpd_kyc-proof.c +++ b/src/exchange/taler-exchange-httpd_kyc-proof.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021 Taler Systems SA + 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 Affero General Public License as published by the Free Software @@ -23,9 +23,12 @@ #include <gnunet/gnunet_json_lib.h> #include <jansson.h> #include <microhttpd.h> -#include <pthread.h> +#include "taler_attributes.h" #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" +#include "taler_templating_lib.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_kyc-proof.h" #include "taler-exchange-httpd_responses.h" @@ -52,35 +55,41 @@ struct KycProofContext struct TEH_RequestContext *rc; /** - * Handle for the OAuth 2.0 CURL request. + * Proof logic to run. */ - struct GNUNET_CURL_Job *job; + struct TALER_KYCLOGIC_Plugin *logic; /** - * OAuth 2.0 authorization code. + * Configuration for @a logic. */ - const char *authorization_code; + struct TALER_KYCLOGIC_ProviderDetails *pd; /** - * OAuth 2.0 token URL we are using for the - * request. + * Asynchronous operation with the proof system. */ - char *token_url; + struct TALER_KYCLOGIC_ProofHandle *ph; /** - * Body of the POST request. + * KYC AML trigger operation. */ - char *post_body; + struct TEH_KycAmlTrigger *kat; /** - * User ID extracted from the OAuth 2.0 service, or NULL. + * Process information about the user for the plugin from the database, can + * be NULL. */ - char *id; + char *provider_user_id; /** - * Payment target this is about. + * Process information about the legitimization process for the plugin from the + * database, can be NULL. */ - unsigned long long payment_target_uuid; + char *provider_legitimization_id; + + /** + * Hash of payment target URI this is about. + */ + struct TALER_PaytoHashP h_payto; /** * HTTP response to return. @@ -88,16 +97,24 @@ struct KycProofContext struct MHD_Response *response; /** + * Provider configuration section name of the logic we are running. + */ + const char *provider_section; + + /** + * Row in the database for this legitimization operation. + */ + uint64_t process_row; + + /** * HTTP response code to return. */ unsigned int response_code; /** - * #GNUNET_YES if we are suspended, - * #GNUNET_NO if not. - * #GNUNET_SYSERR if we had some error. + * True if we are suspended, */ - enum GNUNET_GenericReturnValue suspended; + bool suspended; }; @@ -122,7 +139,7 @@ static void kpc_resume (struct KycProofContext *kpc) { GNUNET_assert (GNUNET_YES == kpc->suspended); - kpc->suspended = GNUNET_NO; + kpc->suspended = false; GNUNET_CONTAINER_DLL_remove (kpc_head, kpc_tail, kpc); @@ -138,10 +155,10 @@ TEH_kyc_proof_cleanup (void) while (NULL != (kpc = kpc_head)) { - if (NULL != kpc->job) + if (NULL != kpc->ph) { - GNUNET_CURL_job_cancel (kpc->job); - kpc->job = NULL; + kpc->logic->proof_cancel (kpc->ph); + kpc->ph = NULL; } kpc_resume (kpc); } @@ -149,348 +166,232 @@ TEH_kyc_proof_cleanup (void) /** - * Function implementing database transaction to check proof's KYC status. - * Runs the transaction logic; IF it returns a non-error code, the transaction - * logic MUST NOT queue a MHD response. IF it returns an hard error, the - * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it - * returns the soft error code, the function MAY be called again to retry and - * MUST not queue a MHD response. + * Function called after the KYC-AML trigger is done. * - * @param cls closure with a `struct KycProofContext *` - * @param connection MHD proof which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - * @return transaction status + * @param cls closure + * @param http_status final HTTP status to return + * @param[in] response final HTTP ro return */ -static enum GNUNET_DB_QueryStatus -persist_kyc_ok (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) +static void +proof_finish ( + void *cls, + unsigned int http_status, + struct MHD_Response *response) { struct KycProofContext *kpc = cls; - enum GNUNET_DB_QueryStatus qs; - qs = TEH_plugin->set_kyc_ok (TEH_plugin->cls, - kpc->payment_target_uuid, - kpc->id); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "set_kyc_ok"); - } - return qs; + kpc->kat = NULL; + kpc->response_code = http_status; + kpc->response = response; + kpc_resume (kpc); } /** - * The request for @a kpc failed. We may have gotten a useful error - * message in @a j. Generate a failure response. + * Generate HTML error for @a connection using @a template. * - * @param[in,out] kpc request that failed - * @param j reply from the server (or NULL) + * @param connection HTTP client connection + * @param template template to expand + * @param[in,out] http_status HTTP status of the response + * @param ec Taler error code to return + * @param message extended message to return + * @return MHD response object */ -static void -handle_error (struct KycProofContext *kpc, - const json_t *j) +struct MHD_Response * +make_html_error (struct MHD_Connection *connection, + const char *template, + unsigned int *http_status, + enum TALER_ErrorCode ec, + const char *message) { - const char *msg; - const char *desc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("error", - &msg), - GNUNET_JSON_spec_string ("error_description", - &desc), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - const char *emsg; - unsigned int line; - - res = GNUNET_JSON_parse (j, - spec, - &emsg, - &line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - return; - } - } - /* case TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_AUTHORZATION_FAILED, - we MAY want to in the future look at the requested content type - and possibly respond in JSON if indicated. */ - { - char *reply; - - GNUNET_asprintf (&reply, - "<html><head><title>%s</title></head><body><h1>%s</h1><p>%s</p></body></html>", - msg, - msg, - desc); - kpc->response - = MHD_create_response_from_buffer (strlen (reply), - reply, - MHD_RESPMEM_MUST_COPY); - GNUNET_assert (NULL != kpc->response); - GNUNET_free (reply); - } - kpc->response_code = MHD_HTTP_FORBIDDEN; + struct MHD_Response *response = NULL; + json_t *body; + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("message", + message)), + TALER_JSON_pack_ec ( + ec)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (connection, + http_status, + template, + NULL, + NULL, + body, + &response)); + json_decref (body); + return response; } /** - * The request for @a kpc succeeded (presumably). - * Parse the user ID and store it in @a kpc (if possible). + * Respond with an HTML message on the given @a rc. * - * @param[in,out] kpc request that succeeded - * @param j reply from the server + * @param[in,out] rc request to respond to + * @param http_status HTTP status code to use + * @param template template to fill in + * @param ec error code to use for the template + * @param message additional message to return + * @return MHD result code */ -static void -parse_success_reply (struct KycProofContext *kpc, - const json_t *j) +static MHD_RESULT +respond_html_ec (struct TEH_RequestContext *rc, + unsigned int http_status, + const char *template, + enum TALER_ErrorCode ec, + const char *message) { - const char *state; - json_t *data; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("status", - &state), - GNUNET_JSON_spec_json ("data", - &data), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - const char *emsg; - unsigned int line; - - res = GNUNET_JSON_parse (j, - spec, - &emsg, - &line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - return; - } - if (0 != strcasecmp (state, - "success")) - { - GNUNET_break_op (0); - handle_error (kpc, - j); - return; - } - { - const char *id; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("id", - &id), - GNUNET_JSON_spec_end () - }; - - res = GNUNET_JSON_parse (data, - ispec, - &emsg, - &line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - return; - } - kpc->id = GNUNET_strdup (id); - } + struct MHD_Response *response; + MHD_RESULT res; + + response = make_html_error (rc->connection, + template, + &http_status, + ec, + message); + res = MHD_queue_response (rc->connection, + http_status, + response); + MHD_destroy_response (response); + return res; } /** - * After we are done with the CURL interaction we - * need to update our database state with the information - * retrieved. + * Function called with the result of a proof check operation. * - * @param cls our `struct KycProofContext` - * @param response_code HTTP response code from server, 0 on hard error - * @param response in JSON, NULL if response was not in JSON format - */ -static void -handle_curl_fetch_finished (void *cls, - long response_code, - const void *response) -{ - struct KycProofContext *kpc = cls; - const json_t *j = response; - - kpc->job = NULL; - switch (response_code) - { - case MHD_HTTP_OK: - parse_success_reply (kpc, - j); - break; - default: - handle_error (kpc, - j); - break; - } - kpc_resume (kpc); -} - - -/** - * After we are done with the CURL interaction we - * need to fetch the user's account details. + * Note that the "decref" for the @a response + * will be done by the callee and MUST NOT be done by the plugin. * - * @param cls our `struct KycProofContext` - * @param response_code HTTP response code from server, 0 on hard error - * @param response in JSON, NULL if response was not in JSON format + * @param cls closure + * @param status KYC status + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client */ static void -handle_curl_login_finished (void *cls, - long response_code, - const void *response) +proof_cb ( + void *cls, + enum TALER_KYCLOGIC_KycStatus status, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response) { struct KycProofContext *kpc = cls; - const json_t *j = response; + struct TEH_RequestContext *rc = kpc->rc; + struct GNUNET_AsyncScopeSave old_scope; - kpc->job = NULL; - switch (response_code) + kpc->ph = NULL; + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); + switch (status) { - case MHD_HTTP_OK: + case TALER_KYCLOGIC_STATUS_SUCCESS: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC process #%llu succeeded with KYC provider\n", + (unsigned long long) kpc->process_row); + kpc->kat = TEH_kyc_finished (&rc->async_scope_id, + kpc->process_row, + &kpc->h_payto, + kpc->provider_section, + provider_user_id, + provider_legitimization_id, + expiration, + attributes, + http_status, + response, + &proof_finish, + kpc); + if (NULL == kpc->kat) { - const char *access_token; - const char *token_type; - uint64_t expires_in_s; - const char *refresh_token; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("access_token", - &access_token), - GNUNET_JSON_spec_string ("token_type", - &token_type), - GNUNET_JSON_spec_uint64 ("expires_in", - &expires_in_s), - GNUNET_JSON_spec_string ("refresh_token", - &refresh_token), - GNUNET_JSON_spec_end () - }; - CURL *eh; - - { - enum GNUNET_GenericReturnValue res; - const char *emsg; - unsigned int line; - - res = GNUNET_JSON_parse (j, - spec, - &emsg, - &line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - break; - } - } - if (0 != strcasecmp (token_type, - "bearer")) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected token type in response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - break; - } - - /* We guard against a few characters that could - conceivably be abused to mess with the HTTP header */ - if ( (NULL != strchr (access_token, - '\n')) || - (NULL != strchr (access_token, - '\r')) || - (NULL != strchr (access_token, - ' ')) || - (NULL != strchr (access_token, - ';')) ) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Illegal character in access token"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - break; - } - - eh = curl_easy_init (); - if (NULL == eh) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "curl_easy_init"); - kpc->response_code - = MHD_HTTP_INTERNAL_SERVER_ERROR; - break; - } - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_URL, - TEH_kyc_config.details.oauth2.info_url)); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + if (NULL != response) + MHD_destroy_response (response); + response = make_html_error (kpc->rc->connection, + "kyc-proof-internal-error", + &http_status, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + "[exchange] AML_KYC_TRIGGER"); + } + break; + case TALER_KYCLOGIC_STATUS_FAILED: + case TALER_KYCLOGIC_STATUS_PROVIDER_FAILED: + case TALER_KYCLOGIC_STATUS_USER_ABORTED: + case TALER_KYCLOGIC_STATUS_ABORTED: + GNUNET_assert (NULL == kpc->kat); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC process %s/%s (Row #%llu) failed: %d\n", + provider_user_id, + provider_legitimization_id, + (unsigned long long) kpc->process_row, + status); + if (5 == http_status / 100) + { + char *msg; + + /* OAuth2 server had a problem, do NOT log this as a KYC failure */ + if (NULL != response) + MHD_destroy_response (response); + GNUNET_asprintf (&msg, + "Failure by KYC provider (HTTP status %u)\n", + http_status); + http_status = MHD_HTTP_BAD_GATEWAY; + response = make_html_error (kpc->rc->connection, + "kyc-proof-internal-error", + &http_status, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + msg); + GNUNET_free (msg); + } + else + { + if (! TEH_kyc_failed (kpc->process_row, + &kpc->h_payto, + kpc->provider_section, + provider_user_id, + provider_legitimization_id)) { - char *hdr; - struct curl_slist *slist; - - GNUNET_asprintf (&hdr, - "%s: Bearer %s", - MHD_HTTP_HEADER_AUTHORIZATION, - access_token); - slist = curl_slist_append (NULL, - hdr); - kpc->job = GNUNET_CURL_job_add2 (TEH_curl_ctx, - eh, - slist, - &handle_curl_fetch_finished, - kpc); - curl_slist_free_all (slist); - GNUNET_free (hdr); + GNUNET_break (0); + if (NULL != response) + MHD_destroy_response (response); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + response = make_html_error (kpc->rc->connection, + "kyc-proof-internal-error", + &http_status, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_failure"); } - return; } + break; default: - handle_error (kpc, - j); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC status of %s/%s (Row #%llu) is %d\n", + provider_user_id, + provider_legitimization_id, + (unsigned long long) kpc->process_row, + (int) status); break; } - kpc_resume (kpc); + if (NULL == kpc->kat) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC process #%llu failed with status %d\n", + (unsigned long long) kpc->process_row, + status); + proof_finish (kpc, + http_status, + response); + } + GNUNET_async_scope_restore (&old_scope); } @@ -504,19 +405,23 @@ clean_kpc (struct TEH_RequestContext *rc) { struct KycProofContext *kpc = rc->rh_ctx; - if (NULL != kpc->job) + if (NULL != kpc->ph) + { + kpc->logic->proof_cancel (kpc->ph); + kpc->ph = NULL; + } + if (NULL != kpc->kat) { - GNUNET_CURL_job_cancel (kpc->job); - kpc->job = NULL; + TEH_kyc_finished_cancel (kpc->kat); + kpc->kat = NULL; } if (NULL != kpc->response) { MHD_destroy_response (kpc->response); kpc->response = NULL; } - GNUNET_free (kpc->post_body); - GNUNET_free (kpc->token_url); - GNUNET_free (kpc->id); + GNUNET_free (kpc->provider_user_id); + GNUNET_free (kpc->provider_legitimization_id); GNUNET_free (kpc); } @@ -524,191 +429,137 @@ clean_kpc (struct TEH_RequestContext *rc) MHD_RESULT TEH_handler_kyc_proof ( struct TEH_RequestContext *rc, - const char *const args[]) + const char *const args[1]) { struct KycProofContext *kpc = rc->rh_ctx; + const char *provider_section_or_logic = args[0]; if (NULL == kpc) - { /* first time */ - char dummy; - + { + /* first time */ + if (NULL == provider_section_or_logic) + { + GNUNET_break_op (0); + return respond_html_ec (rc, + MHD_HTTP_NOT_FOUND, + "kyc-proof-endpoint-unknown", + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + "'/kyc-proof/$PROVIDER_SECTION?state=$H_PAYTO' required"); + } kpc = GNUNET_new (struct KycProofContext); kpc->rc = rc; rc->rh_ctx = kpc; rc->rh_cleaner = &clean_kpc; - - if (1 != - sscanf (args[0], - "%llu%c", - &kpc->payment_target_uuid, - &dummy)) + TALER_MHD_parse_request_arg_auto_t (rc->connection, + "state", + &kpc->h_payto); + if (GNUNET_OK != + TALER_KYCLOGIC_lookup_logic (provider_section_or_logic, + &kpc->logic, + &kpc->pd, + &kpc->provider_section)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payment_target_uuid"); + return respond_html_ec (rc, + MHD_HTTP_NOT_FOUND, + "kyc-proof-target-unknown", + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + provider_section_or_logic); } - kpc->authorization_code - = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, - "code"); - if (NULL == kpc->authorization_code) + if (NULL != kpc->provider_section) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "code"); - } - if (TEH_KYC_NONE == TEH_kyc_config.mode) - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute expiration; - { - CURL *eh; + if (0 != strcmp (provider_section_or_logic, + kpc->provider_section)) + { + GNUNET_break_op (0); + return respond_html_ec (rc, + MHD_HTTP_BAD_REQUEST, + "kyc-proof-bad-request", + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "PROVIDER_SECTION"); + } - eh = curl_easy_init (); - if (NULL == eh) + qs = TEH_plugin->lookup_kyc_process_by_account ( + TEH_plugin->cls, + kpc->provider_section, + &kpc->h_payto, + &kpc->process_row, + &expiration, + &kpc->provider_user_id, + &kpc->provider_legitimization_id); + switch (qs) { - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "curl_easy_init"); + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + return respond_html_ec (rc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "kyc-proof-internal-error", + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_kyc_process_by_account"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return respond_html_ec (rc, + MHD_HTTP_NOT_FOUND, + "kyc-proof-target-unknown", + TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, + kpc->provider_section); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; } - GNUNET_asprintf (&kpc->token_url, - "%stoken", - TEH_kyc_config.details.oauth2.url); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_URL, - kpc->token_url)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_POST, - 1)); + if (GNUNET_TIME_absolute_is_future (expiration)) { - char *client_id; - char *redirect_uri; - char *client_secret; - char *authorization_code; - - client_id = curl_easy_escape (eh, - TEH_kyc_config.details.oauth2.client_id, - 0); - GNUNET_assert (NULL != client_id); - { - char *request_uri; - - GNUNET_asprintf (&request_uri, - "%slogin?client_id=%s", - TEH_kyc_config.details.oauth2.url, - TEH_kyc_config.details.oauth2.client_id); - redirect_uri = curl_easy_escape (eh, - request_uri, - 0); - GNUNET_free (request_uri); - } - GNUNET_assert (NULL != redirect_uri); - client_secret = curl_easy_escape (eh, - TEH_kyc_config.details.oauth2. - client_secret, - 0); - GNUNET_assert (NULL != client_secret); - authorization_code = curl_easy_escape (eh, - kpc->authorization_code, - 0); - GNUNET_assert (NULL != authorization_code); - GNUNET_asprintf (&kpc->post_body, - "client_id=%s&redirect_uri=%s&client_secret=%s&code=%s&grant_type=authorization_code", - client_id, - redirect_uri, - client_secret, - authorization_code); - curl_free (authorization_code); - curl_free (client_secret); - curl_free (redirect_uri); - curl_free (client_id); + /* KYC not required */ + return respond_html_ec (rc, + MHD_HTTP_OK, + "kyc-proof-already-done", + TALER_EC_NONE, + NULL); } - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_POSTFIELDS, - kpc->post_body)); - 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)); - - kpc->job = GNUNET_CURL_job_add (TEH_curl_ctx, - eh, - &handle_curl_login_finished, - kpc); - kpc->suspended = GNUNET_YES; - GNUNET_CONTAINER_DLL_insert (kpc_head, - kpc_tail, - kpc); - MHD_suspend_connection (rc->connection); - return MHD_YES; } - } + kpc->ph = kpc->logic->proof (kpc->logic->cls, + kpc->pd, + rc->connection, + &kpc->h_payto, + kpc->process_row, + kpc->provider_user_id, + kpc->provider_legitimization_id, + &proof_cb, + kpc); + if (NULL == kpc->ph) + { + GNUNET_break (0); + return respond_html_ec (rc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "kyc-proof-internal-error", + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "could not start proof with KYC logic"); + } - if (NULL != kpc->response) - { - /* handle _failed_ resumed cases */ - return MHD_queue_response (rc->connection, - kpc->response_code, - kpc->response); - } - /* _successfully_ resumed case */ - { - MHD_RESULT res; - enum GNUNET_GenericReturnValue ret; - - ret = TEH_DB_run_transaction (kpc->rc->connection, - "check proof kyc", - TEH_MT_OTHER, - &res, - &persist_kyc_ok, - kpc); - if (GNUNET_SYSERR == ret) - return res; + kpc->suspended = true; + GNUNET_CONTAINER_DLL_insert (kpc_head, + kpc_tail, + kpc); + MHD_suspend_connection (rc->connection); + return MHD_YES; } + if (NULL == kpc->response) { - struct MHD_Response *response; - MHD_RESULT res; - - response = MHD_create_response_from_buffer (0, - "", - MHD_RESPMEM_PERSISTENT); - if (NULL == response) - { - GNUNET_break (0); - return MHD_NO; - } - GNUNET_break (MHD_YES == - MHD_add_response_header ( - response, - MHD_HTTP_HEADER_LOCATION, - TEH_kyc_config.details.oauth2.post_kyc_redirect_url)); - res = MHD_queue_response (rc->connection, - MHD_HTTP_SEE_OTHER, - response); - MHD_destroy_response (response); - return res; + GNUNET_break (0); + return respond_html_ec (rc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "kyc-proof-internal-error", + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "handler resumed without response"); } + + /* return response from KYC logic */ + return MHD_queue_response (rc->connection, + kpc->response_code, + kpc->response); } diff --git a/src/exchange/taler-exchange-httpd_kyc-proof.h b/src/exchange/taler-exchange-httpd_kyc-proof.h index c075b2424..d40ea90a9 100644 --- a/src/exchange/taler-exchange-httpd_kyc-proof.h +++ b/src/exchange/taler-exchange-httpd_kyc-proof.h @@ -37,13 +37,13 @@ TEH_kyc_proof_cleanup (void); * Handle a "/kyc-proof" request. * * @param rc request to handle - * @param args one argument with the payment_target_uuid + * @param args one argument with the legitimization_uuid * @return MHD result code */ MHD_RESULT TEH_handler_kyc_proof ( struct TEH_RequestContext *rc, - const char *const args[]); + const char *const args[1]); #endif diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.c b/src/exchange/taler-exchange-httpd_kyc-wallet.c index 4062f9305..21d07422d 100644 --- a/src/exchange/taler-exchange-httpd_kyc-wallet.c +++ b/src/exchange/taler-exchange-httpd_kyc-wallet.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021 Taler Systems SA + 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 Affero General Public License as published by the Free Software @@ -26,6 +26,7 @@ #include <pthread.h> #include "taler_json_lib.h" #include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h" #include "taler-exchange-httpd_kyc-wallet.h" #include "taler-exchange-httpd_responses.h" @@ -38,16 +39,60 @@ struct KycRequestContext /** * Public key of the reserve/wallet this is about. */ + struct TALER_PaytoHashP h_payto; + + /** + * The reserve's public key + */ struct TALER_ReservePublicKeyP reserve_pub; /** - * Current KYC status. + * KYC status, with row with the legitimization requirement. */ struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Balance threshold crossed by the wallet. + */ + struct TALER_Amount balance; + + /** + * Name of the required check. + */ + char *required; + }; /** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Returns the wallet balance. + * + * @param cls closure, a `struct KycRequestContext` + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +balance_iterator (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct KycRequestContext *krc = cls; + + (void) limit; + cb (cb_cls, + &krc->balance, + GNUNET_TIME_absolute_get ()); +} + + +/** * Function implementing database transaction to check wallet's KYC status. * Runs the transaction logic; IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns an hard error, the @@ -69,9 +114,14 @@ wallet_kyc_check (void *cls, struct KycRequestContext *krc = cls; enum GNUNET_DB_QueryStatus qs; - qs = TEH_plugin->inselect_wallet_kyc_status (TEH_plugin->cls, - &krc->reserve_pub, - &krc->kyc); + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, + &krc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &balance_iterator, + krc, + &krc->required); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -80,9 +130,40 @@ wallet_kyc_check (void *cls, *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "inselect_wallet_status"); + "kyc_test_required"); return qs; } + if (NULL == krc->required) + { + krc->kyc.ok = true; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check required at %s is `%s'\n", + TALER_amount2s (&krc->balance), + krc->required); + krc->kyc.ok = false; + qs = TEH_plugin->insert_kyc_requirement_for_account (TEH_plugin->cls, + krc->required, + &krc->h_payto, + &krc->reserve_pub, + &krc->kyc.requirement_row); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "insert_kyc_requirement_for_account"); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC requirement inserted for wallet %s (%llu, %d)\n", + TALER_B2S (&krc->h_payto), + (unsigned long long) krc->kyc.requirement_row, + qs); return qs; } @@ -100,14 +181,13 @@ TEH_handler_kyc_wallet ( &reserve_sig), GNUNET_JSON_spec_fixed_auto ("reserve_pub", &krc.reserve_pub), + TALER_JSON_spec_amount ("balance", + TEH_currency, + &krc.balance), GNUNET_JSON_spec_end () }; MHD_RESULT res; enum GNUNET_GenericReturnValue ret; - struct GNUNET_CRYPTO_EccSignaturePurpose purpose = { - .size = htonl (sizeof (purpose)), - .purpose = htonl (TALER_SIGNATURE_WALLET_ACCOUNT_SETUP) - }; (void) args; ret = TALER_MHD_parse_json_data (rc->connection, @@ -118,11 +198,11 @@ TEH_handler_kyc_wallet ( if (GNUNET_NO == ret) return MHD_YES; /* failure */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify_ (TALER_SIGNATURE_WALLET_ACCOUNT_SETUP, - &purpose, - &reserve_sig.eddsa_signature, - &krc.reserve_pub.eddsa_pub)) + TALER_wallet_account_setup_verify (&krc.reserve_pub, + &krc.balance, + &reserve_sig)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( @@ -131,26 +211,41 @@ TEH_handler_kyc_wallet ( TALER_EC_EXCHANGE_KYC_WALLET_SIGNATURE_INVALID, NULL); } - if (TEH_KYC_NONE == TEH_kyc_config.mode) - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + { + char *payto_uri; + + payto_uri = TALER_reserve_make_payto (TEH_base_url, + &krc.reserve_pub); + TALER_payto_hash (payto_uri, + &krc.h_payto); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "h_payto of wallet %s is %s\n", + payto_uri, + TALER_B2S (&krc.h_payto)); + GNUNET_free (payto_uri); + } ret = TEH_DB_run_transaction (rc->connection, "check wallet kyc", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &res, &wallet_kyc_check, &krc); if (GNUNET_SYSERR == ret) return res; - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - krc.kyc.payment_target_uuid)); + if (NULL == krc.required) + { + /* KYC not required or already satisfied */ + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_free (krc.required); + return TEH_RESPONSE_reply_kyc_required (rc->connection, + &krc.h_payto, + &krc.kyc); } diff --git a/src/exchange/taler-exchange-httpd_kyc-webhook.c b/src/exchange/taler-exchange-httpd_kyc-webhook.c new file mode 100644 index 000000000..b92b43e69 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_kyc-webhook.c @@ -0,0 +1,420 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_kyc-webhook.c + * @brief Handle notification of KYC completion via webhook. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_attributes.h" +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_kyc-webhook.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Context for the webhook. + */ +struct KycWebhookContext +{ + + /** + * Kept in a DLL while suspended. + */ + struct KycWebhookContext *next; + + /** + * Kept in a DLL while suspended. + */ + struct KycWebhookContext *prev; + + /** + * Details about the connection we are processing. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for the KYC-AML trigger interaction. + */ + struct TEH_KycAmlTrigger *kat; + + /** + * Plugin responsible for the webhook. + */ + struct TALER_KYCLOGIC_Plugin *plugin; + + /** + * Section in the configuration of the configured + * KYC provider. + */ + const char *provider_section; + + /** + * Configuration for the specific action. + */ + struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Webhook activity. + */ + struct TALER_KYCLOGIC_WebhookHandle *wh; + + /** + * HTTP response to return. + */ + struct MHD_Response *response; + + /** + * HTTP response code to return. + */ + unsigned int response_code; + + /** + * #GNUNET_YES if we are suspended, + * #GNUNET_NO if not. + * #GNUNET_SYSERR if we had some error. + */ + enum GNUNET_GenericReturnValue suspended; + +}; + + +/** + * Contexts are kept in a DLL while suspended. + */ +static struct KycWebhookContext *kwh_head; + +/** + * Contexts are kept in a DLL while suspended. + */ +static struct KycWebhookContext *kwh_tail; + + +/** + * Resume processing the @a kwh request. + * + * @param kwh request to resume + */ +static void +kwh_resume (struct KycWebhookContext *kwh) +{ + GNUNET_assert (GNUNET_YES == kwh->suspended); + kwh->suspended = GNUNET_NO; + GNUNET_CONTAINER_DLL_remove (kwh_head, + kwh_tail, + kwh); + MHD_resume_connection (kwh->rc->connection); + TALER_MHD_daemon_trigger (); +} + + +void +TEH_kyc_webhook_cleanup (void) +{ + struct KycWebhookContext *kwh; + + while (NULL != (kwh = kwh_head)) + { + if (NULL != kwh->wh) + { + kwh->plugin->webhook_cancel (kwh->wh); + kwh->wh = NULL; + } + kwh_resume (kwh); + } +} + + +/** + * Function called after the KYC-AML trigger is done. + * + * @param cls closure with a `struct KycWebhookContext *` + * @param http_status final HTTP status to return + * @param[in] response final HTTP ro return + */ +static void +kyc_aml_webhook_finished ( + void *cls, + unsigned int http_status, + struct MHD_Response *response) +{ + struct KycWebhookContext *kwh = cls; + + kwh->kat = NULL; + kwh->response = response; + kwh->response_code = http_status; + kwh_resume (kwh); +} + + +/** + * Function called with the result of a KYC webhook operation. + * + * Note that the "decref" for the @a response + * will be done by the plugin. + * + * @param cls closure + * @param process_row legitimization process the webhook was about + * @param account_id account the webhook was about + * @param provider_section name of the configuration section of the logic that was run + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param status KYC status + * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client + */ +static void +webhook_finished_cb ( + void *cls, + uint64_t process_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_section, + const char *provider_user_id, + const char *provider_legitimization_id, + enum TALER_KYCLOGIC_KycStatus status, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response) +{ + struct KycWebhookContext *kwh = cls; + + kwh->wh = NULL; + switch (status) + { + case TALER_KYCLOGIC_STATUS_SUCCESS: + kwh->kat = TEH_kyc_finished ( + &kwh->rc->async_scope_id, + process_row, + account_id, + provider_section, + provider_user_id, + provider_legitimization_id, + expiration, + attributes, + http_status, + response, + &kyc_aml_webhook_finished, + kwh); + if (NULL == kwh->kat) + { + if (NULL != response) + MHD_destroy_response (response); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + response = TALER_MHD_make_error ( + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + "[exchange] AML_KYC_TRIGGER"); + break; + } + return; + case TALER_KYCLOGIC_STATUS_FAILED: + case TALER_KYCLOGIC_STATUS_PROVIDER_FAILED: + case TALER_KYCLOGIC_STATUS_USER_ABORTED: + case TALER_KYCLOGIC_STATUS_ABORTED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC process %s/%s (Row #%llu) failed: %d\n", + provider_user_id, + provider_legitimization_id, + (unsigned long long) process_row, + status); + if (! TEH_kyc_failed (process_row, + account_id, + provider_section, + provider_user_id, + provider_legitimization_id)) + { + GNUNET_break (0); + if (NULL != response) + MHD_destroy_response (response); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + response = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_failure"); + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC status of %s/%s (Row #%llu) is %d\n", + provider_user_id, + provider_legitimization_id, + (unsigned long long) process_row, + (int) status); + break; + } + GNUNET_break (NULL == kwh->kat); + kyc_aml_webhook_finished (kwh, + http_status, + response); +} + + +/** + * Function called to clean up a context. + * + * @param rc request context + */ +static void +clean_kwh (struct TEH_RequestContext *rc) +{ + struct KycWebhookContext *kwh = rc->rh_ctx; + + if (NULL != kwh->wh) + { + kwh->plugin->webhook_cancel (kwh->wh); + kwh->wh = NULL; + } + if (NULL != kwh->kat) + { + TEH_kyc_finished_cancel (kwh->kat); + kwh->kat = NULL; + } + if (NULL != kwh->response) + { + MHD_destroy_response (kwh->response); + kwh->response = NULL; + } + GNUNET_free (kwh); +} + + +/** + * Handle a (GET or POST) "/kyc-webhook" request. + * + * @param rc request to handle + * @param method HTTP request method used by the client + * @param root uploaded JSON body (can be NULL) + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +static MHD_RESULT +handler_kyc_webhook_generic ( + struct TEH_RequestContext *rc, + const char *method, + const json_t *root, + const char *const args[]) +{ + struct KycWebhookContext *kwh = rc->rh_ctx; + + if (NULL == kwh) + { /* first time */ + kwh = GNUNET_new (struct KycWebhookContext); + kwh->rc = rc; + rc->rh_ctx = kwh; + rc->rh_cleaner = &clean_kwh; + + if ( (NULL == args[0]) || + (GNUNET_OK != + TALER_KYCLOGIC_lookup_logic (args[0], + &kwh->plugin, + &kwh->pd, + &kwh->provider_section)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC logic `%s' unknown (check KYC provider configuration)\n", + args[0]); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + args[0]); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC logic `%s' mapped to section %s\n", + args[0], + kwh->provider_section); + kwh->wh = kwh->plugin->webhook (kwh->plugin->cls, + kwh->pd, + TEH_plugin->kyc_provider_account_lookup, + TEH_plugin->cls, + method, + &args[1], + rc->connection, + root, + &webhook_finished_cb, + kwh); + if (NULL == kwh->wh) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "failed to run webhook logic"); + } + kwh->suspended = GNUNET_YES; + GNUNET_CONTAINER_DLL_insert (kwh_head, + kwh_tail, + kwh); + MHD_suspend_connection (rc->connection); + return MHD_YES; + } + GNUNET_break (GNUNET_NO == kwh->suspended); + + if (NULL != kwh->response) + { + MHD_RESULT res; + + res = MHD_queue_response (rc->connection, + kwh->response_code, + kwh->response); + GNUNET_break (MHD_YES == res); + return res; + } + + /* We resumed, but got no response? This should + not happen. */ + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "resumed without response"); +} + + +MHD_RESULT +TEH_handler_kyc_webhook_get ( + struct TEH_RequestContext *rc, + const char *const args[]) +{ + return handler_kyc_webhook_generic (rc, + MHD_HTTP_METHOD_GET, + NULL, + args); +} + + +MHD_RESULT +TEH_handler_kyc_webhook_post ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + return handler_kyc_webhook_generic (rc, + MHD_HTTP_METHOD_POST, + root, + args); +} + + +/* end of taler-exchange-httpd_kyc-webhook.c */ diff --git a/src/exchange/taler-exchange-httpd_kyc-webhook.h b/src/exchange/taler-exchange-httpd_kyc-webhook.h new file mode 100644 index 000000000..ea3821897 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_kyc-webhook.h @@ -0,0 +1,64 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_kyc-webhook.h + * @brief Handle /kyc-webhook requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_KYC_WEBHOOK_H +#define TALER_EXCHANGE_HTTPD_KYC_WEBHOOK_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown kyc-webhook subsystem. Resumes all suspended long-polling clients + * and cleans up data structures. + */ +void +TEH_kyc_webhook_cleanup (void); + + +/** + * Handle a GET "/kyc-webhook" request. + * + * @param rc request to handle + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_webhook_get ( + struct TEH_RequestContext *rc, + const char *const args[]); + + +/** + * Handle a POST "/kyc-webhook" request. + * + * @param rc request to handle + * @param root uploaded JSON body + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_webhook_post ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_link.c b/src/exchange/taler-exchange-httpd_link.c index d3c0d6a5a..3d92a11a3 100644 --- a/src/exchange/taler-exchange-httpd_link.c +++ b/src/exchange/taler-exchange-httpd_link.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2019 Taler Systems SA + Copyright (C) 2014-2019, 2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -39,7 +39,7 @@ struct HTD_Context /** * Public key of the coin for which we are running link. */ - struct TALER_CoinSpendPublicKeyP coin_pub; + const struct TALER_CoinSpendPublicKeyP *coin_pub; /** * Json array with transfer data we collect. @@ -86,8 +86,18 @@ handle_link_data (void *cls, &pos->denom_pub), TALER_JSON_pack_blinded_denom_sig ("ev_sig", &pos->ev_sig), + GNUNET_JSON_pack_uint64 ("coin_idx", + pos->coin_refresh_offset), + TALER_JSON_pack_exchange_withdraw_values ("ewv", + &pos->alg_values), GNUNET_JSON_pack_data_auto ("link_sig", - &pos->orig_coin_link_sig)); + &pos->orig_coin_link_sig), + GNUNET_JSON_pack_allow_null ( + pos->have_nonce + ? GNUNET_JSON_pack_data_auto ("cs_nonce", + &pos->nonce) + : GNUNET_JSON_pack_string ("cs_nonce", + NULL))); if ( (NULL == obj) || (0 != json_array_append_new (list, @@ -143,7 +153,7 @@ link_transaction (void *cls, enum GNUNET_DB_QueryStatus qs; qs = TEH_plugin->get_link_data (TEH_plugin->cls, - &ctx->coin_pub, + ctx->coin_pub, &handle_link_data, ctx); if (NULL == ctx->mlist) @@ -168,32 +178,19 @@ link_transaction (void *cls, MHD_RESULT TEH_handler_link (struct TEH_RequestContext *rc, - const char *const args[2]) + const struct TALER_CoinSpendPublicKeyP *coin_pub) { - struct HTD_Context ctx; + struct HTD_Context ctx = { + .coin_pub = coin_pub + }; MHD_RESULT mhd_ret; - memset (&ctx, - 0, - sizeof (ctx)); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &ctx.coin_pub, - sizeof (ctx.coin_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB, - args[0]); - } ctx.mlist = json_array (); GNUNET_assert (NULL != ctx.mlist); if (GNUNET_OK != TEH_DB_run_transaction (rc->connection, "run link", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &mhd_ret, &link_transaction, &ctx)) diff --git a/src/exchange/taler-exchange-httpd_link.h b/src/exchange/taler-exchange-httpd_link.h index 01679e877..255c0ca57 100644 --- a/src/exchange/taler-exchange-httpd_link.h +++ b/src/exchange/taler-exchange-httpd_link.h @@ -32,12 +32,12 @@ * Handle a "/coins/$COIN_PUB/link" request. * * @param rc request context - * @param args array of additional options (length: 2, first is the coin_pub, second must be "link") + * @param coin_pub the coin public key * @return MHD result code */ MHD_RESULT TEH_handler_link (struct TEH_RequestContext *rc, - const char *const args[2]); + const struct TALER_CoinSpendPublicKeyP *coin_pub); #endif diff --git a/src/exchange/taler-exchange-httpd_management.h b/src/exchange/taler-exchange-httpd_management.h index f0d922e64..2fc1fe8db 100644 --- a/src/exchange/taler-exchange-httpd_management.h +++ b/src/exchange/taler-exchange-httpd_management.h @@ -64,7 +64,7 @@ TEH_handler_management_auditors_AP_disable ( MHD_RESULT TEH_handler_management_denominations_HDP_revoke ( struct MHD_Connection *connection, - const struct TALER_DenominationHash *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const json_t *root); @@ -136,6 +136,19 @@ TEH_handler_management_post_wire_fees ( /** + * Handle a POST "/management/global-fees" request. + * + * @param connection the MHD connection to handle + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_management_post_global_fees ( + struct MHD_Connection *connection, + const json_t *root); + + +/** * Handle a POST "/management/extensions" request. * * @param connection the MHD connection to handle @@ -149,6 +162,45 @@ TEH_handler_management_post_extensions ( /** + * Handle a POST "/management/drain" request. + * + * @param connection the MHD connection to handle + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_management_post_drain ( + struct MHD_Connection *connection, + const json_t *root); + + +/** + * Handle a POST "/management/aml-officers" request. + * + * @param connection the MHD connection to handle + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_management_aml_officers ( + struct MHD_Connection *connection, + const json_t *root); + + +/** + * Handle a POST "/management/partners" request. + * + * @param connection the MHD connection to handle + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_management_partners ( + struct MHD_Connection *connection, + const json_t *root); + + +/** * Initialize extension configuration handling. * * @return #GNUNET_OK on success diff --git a/src/exchange/taler-exchange-httpd_management_aml-officers.c b/src/exchange/taler-exchange-httpd_management_aml-officers.c new file mode 100644 index 000000000..abc7c3d84 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_management_aml-officers.c @@ -0,0 +1,142 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_management_aml-officers.c + * @brief Handle request to update AML officer status + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How often do we try the DB operation at most? + */ +#define MAX_RETRIES 10 + + +MHD_RESULT +TEH_handler_management_aml_officers ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct TALER_AmlOfficerPublicKeyP officer_pub; + const char *officer_name; + struct GNUNET_TIME_Timestamp change_date; + bool is_active; + bool read_only; + struct TALER_MasterSignatureP master_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("officer_pub", + &officer_pub), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + GNUNET_JSON_spec_bool ("is_active", + &is_active), + GNUNET_JSON_spec_bool ("read_only", + &read_only), + GNUNET_JSON_spec_string ("officer_name", + &officer_name), + GNUNET_JSON_spec_timestamp ("change_date", + &change_date), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_aml_officer_status_verify ( + &officer_pub, + officer_name, + change_date, + is_active, + read_only, + &TEH_master_public_key, + &master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID, + NULL); + } + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp last_date; + unsigned int retries_left = MAX_RETRIES; + + do { + qs = TEH_plugin->insert_aml_officer (TEH_plugin->cls, + &officer_pub, + &master_sig, + officer_name, + is_active, + read_only, + change_date, + &last_date); + if (0 == --retries_left) + break; + } while (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_aml_officer"); + } + if (GNUNET_TIME_timestamp_cmp (last_date, + >, + change_date)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT, + NULL); + } + } + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_aml-officers.c */ diff --git a/src/exchange/taler-exchange-httpd_management_auditors.c b/src/exchange/taler-exchange-httpd_management_auditors.c index 6096cc98b..7e0593534 100644 --- a/src/exchange/taler-exchange-httpd_management_auditors.c +++ b/src/exchange/taler-exchange-httpd_management_auditors.c @@ -153,7 +153,7 @@ TEH_handler_management_auditors ( &aac.master_sig), GNUNET_JSON_spec_fixed_auto ("auditor_pub", &aac.auditor_pub), - GNUNET_JSON_spec_string ("auditor_url", + TALER_JSON_spec_web_url ("auditor_url", &aac.auditor_url), GNUNET_JSON_spec_string ("auditor_name", &aac.auditor_name), @@ -189,7 +189,7 @@ TEH_handler_management_auditors ( ret = TEH_DB_run_transaction (connection, "add auditor", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &res, &add_auditor, &aac); diff --git a/src/exchange/taler-exchange-httpd_management_auditors_AP_disable.c b/src/exchange/taler-exchange-httpd_management_auditors_AP_disable.c index 5ae0cbd07..07e2933bb 100644 --- a/src/exchange/taler-exchange-httpd_management_auditors_AP_disable.c +++ b/src/exchange/taler-exchange-httpd_management_auditors_AP_disable.c @@ -178,7 +178,7 @@ TEH_handler_management_auditors_AP_disable ( ret = TEH_DB_run_transaction (connection, "del auditor", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &res, &del_auditor, &dac); diff --git a/src/exchange/taler-exchange-httpd_management_denominations_HDP_revoke.c b/src/exchange/taler-exchange-httpd_management_denominations_HDP_revoke.c index a8acf2f7a..32da72fbd 100644 --- a/src/exchange/taler-exchange-httpd_management_denominations_HDP_revoke.c +++ b/src/exchange/taler-exchange-httpd_management_denominations_HDP_revoke.c @@ -34,7 +34,7 @@ MHD_RESULT TEH_handler_management_denominations_HDP_revoke ( struct MHD_Connection *connection, - const struct TALER_DenominationHash *h_denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, const json_t *root) { struct TALER_MasterSignatureP master_sig; @@ -56,6 +56,7 @@ TEH_handler_management_denominations_HDP_revoke ( if (GNUNET_NO == res) return MHD_YES; /* failure */ } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_exchange_offline_denomination_revoke_verify ( h_denom_pub, diff --git a/src/exchange/taler-exchange-httpd_management_drain.c b/src/exchange/taler-exchange-httpd_management_drain.c new file mode 100644 index 000000000..1e490d799 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_management_drain.c @@ -0,0 +1,195 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_management_drain.c + * @brief Handle request to drain profits + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for the #drain transaction. + */ +struct DrainContext +{ + /** + * Fee's signature affirming the #TALER_SIGNATURE_MASTER_DRAIN_PROFITS operation. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Wire transfer identifier to use. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Account to credit. + */ + const char *payto_uri; + + /** + * Configuration section with account to debit. + */ + const char *account_section; + + /** + * Signature time. + */ + struct GNUNET_TIME_Timestamp date; + + /** + * Amount to transfer. + */ + struct TALER_Amount amount; + +}; + + +/** + * Function implementing database transaction to drain profits. Runs the + * transaction logic; IF it returns a non-error code, the transaction logic + * MUST NOT queue a MHD response. IF it returns an hard error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it + * returns the soft error code, the function MAY be called again to retry and + * MUST not queue a MHD response. + * + * @param cls closure with a `struct DrainContext` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +drain (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct DrainContext *dc = cls; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->insert_drain_profit ( + TEH_plugin->cls, + &dc->wtid, + dc->account_section, + dc->payto_uri, + dc->date, + &dc->amount, + &dc->master_sig); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert drain profit"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_management_post_drain ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct DrainContext dc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("debit_account_section", + &dc.account_section), + TALER_JSON_spec_payto_uri ("credit_payto_uri", + &dc.payto_uri), + GNUNET_JSON_spec_fixed_auto ("wtid", + &dc.wtid), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &dc.master_sig), + GNUNET_JSON_spec_timestamp ("date", + &dc.date), + TALER_JSON_spec_amount ("amount", + TEH_currency, + &dc.amount), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_profit_drain_verify ( + &dc.wtid, + dc.date, + &dc.amount, + dc.account_section, + dc.payto_uri, + &TEH_master_public_key, + &dc.master_sig)) + { + /* signature invalid */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID, + NULL); + } + + { + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + + res = TEH_DB_run_transaction (connection, + "insert drain profit", + TEH_MT_REQUEST_OTHER, + &ret, + &drain, + &dc); + if (GNUNET_SYSERR == res) + return ret; + } + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_drain.c */ diff --git a/src/exchange/taler-exchange-httpd_management_extensions.c b/src/exchange/taler-exchange-httpd_management_extensions.c index 96b855c3c..3b24bace7 100644 --- a/src/exchange/taler-exchange-httpd_management_extensions.c +++ b/src/exchange/taler-exchange-httpd_management_extensions.c @@ -31,7 +31,6 @@ #include "taler_extensions.h" #include "taler_dbevents.h" - /** * Extension carries the necessary data for a particular extension. * @@ -39,7 +38,7 @@ struct Extension { enum TALER_Extension_Type type; - json_t *config; + json_t *manifest; }; /** @@ -49,43 +48,11 @@ struct SetExtensionsContext { uint32_t num_extensions; struct Extension *extensions; - struct TALER_MasterSignatureP *extensions_sigs; + struct TALER_MasterSignatureP extensions_sig; }; - -/** - * @brief verifies the signature a configuration with the offline master key. - * - * @param config configuration of an extension given as JSON object - * @param master_priv offline master public key of the exchange - * @param[out] master_sig signature - * @return GNUNET_OK on success, GNUNET_SYSERR otherwise - */ -static enum GNUNET_GenericReturnValue -config_verify ( - const json_t *config, - const struct TALER_MasterPublicKeyP *master_pub, - const struct TALER_MasterSignatureP *master_sig - ) -{ - enum GNUNET_GenericReturnValue ret; - struct TALER_ExtensionConfigHash h_config; - - ret = TALER_extension_config_hash (config, &h_config); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - return ret; - } - - return TALER_exchange_offline_extension_config_hash_verify (h_config, - master_pub, - master_sig); -} - - /** - * Function implementing database transaction to set the configuration of + * Function implementing database transaction to set the manifests of * extensions. It runs the transaction logic. * - IF it returns a non-error code, the transaction logic MUST NOT queue a * MHD response. @@ -107,25 +74,24 @@ set_extensions (void *cls, { struct SetExtensionsContext *sec = cls; - /* save the configurations of all extensions */ + /* save the manifests of all extensions */ for (uint32_t i = 0; i<sec->num_extensions; i++) { struct Extension *ext = &sec->extensions[i]; - struct TALER_MasterSignatureP *sig = &sec->extensions_sigs[i]; + const struct TALER_Extension *taler_ext; enum GNUNET_DB_QueryStatus qs; - char *config; + char *manifest; - /* Sanity check. - * TODO: replace with general API to retrieve the extension-handler - */ - if (0 > ext->type || TALER_Extension_Max <= ext->type) + taler_ext = TALER_extensions_get_by_type (ext->type); + if (NULL == taler_ext) { + /* No such extension found */ GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } - config = json_dumps (ext->config, JSON_COMPACT | JSON_SORT_KEYS); - if (NULL == config) + manifest = json_dumps (ext->manifest, JSON_COMPACT | JSON_SORT_KEYS); + if (NULL == manifest) { GNUNET_break (0); *mhd_ret = TALER_MHD_reply_with_error (connection, @@ -135,11 +101,12 @@ set_extensions (void *cls, return GNUNET_DB_STATUS_HARD_ERROR; } - qs = TEH_plugin->set_extension_config ( + qs = TEH_plugin->set_extension_manifest ( TEH_plugin->cls, - TEH_extensions[ext->type]->name, - config, - sig); + taler_ext->name, + manifest); + + free (manifest); if (qs < 0) { @@ -154,41 +121,96 @@ set_extensions (void *cls, /* Success, trigger event */ { - enum TALER_Extension_Type *type = &sec->extensions[i].type; + uint32_t nbo_type = htonl (sec->extensions[i].type); struct GNUNET_DB_EventHeaderP ev = { .size = htons (sizeof (ev)), .type = htons (TALER_DBEVENT_EXCHANGE_EXTENSIONS_UPDATED) }; + TEH_plugin->event_notify (TEH_plugin->cls, &ev, - type, - sizeof(*type)); + &nbo_type, + sizeof(nbo_type)); } } + /* All extensions configured, update the signature */ + TEH_extensions_sig = sec->extensions_sig; + TEH_extensions_signed = true; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* only 'success', so >=0, matters here */ } +static enum GNUNET_GenericReturnValue +verify_extensions_from_json ( + const json_t *extensions, + struct SetExtensionsContext *sec) +{ + const char*name; + const struct TALER_Extension *extension; + size_t i = 0; + json_t *manifest; + + GNUNET_assert (NULL != extensions); + GNUNET_assert (json_is_object (extensions)); + + sec->num_extensions = json_object_size (extensions); + sec->extensions = GNUNET_new_array (sec->num_extensions, + struct Extension); + + json_object_foreach ((json_t *) extensions, name, manifest) + { + int critical = 0; + json_t *config; + const char *version = NULL; + + /* load and verify criticality, version, etc. */ + extension = TALER_extensions_get_by_name (name); + if (NULL == extension) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "no such extension: %s\n", name); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_extensions_parse_manifest ( + manifest, &critical, &version, &config)) + return GNUNET_SYSERR; + + if (critical != extension->critical + || 0 != strcmp (version, extension->version) // FIXME-oec: libtool compare + || NULL == config + || GNUNET_OK != extension->load_config (config, NULL)) + return GNUNET_SYSERR; + + sec->extensions[i].type = extension->type; + sec->extensions[i].manifest = json_copy (manifest); + } + + return GNUNET_OK; +} + + MHD_RESULT TEH_handler_management_post_extensions ( struct MHD_Connection *connection, const json_t *root) { + MHD_RESULT ret; + const json_t *extensions; struct SetExtensionsContext sec = {0}; - json_t *extensions; - json_t *extensions_sigs; struct GNUNET_JSON_Specification top_spec[] = { - GNUNET_JSON_spec_json ("extensions", - &extensions), - GNUNET_JSON_spec_json ("extensions_sigs", - &extensions_sigs), + GNUNET_JSON_spec_object_const ("extensions", + &extensions), + GNUNET_JSON_spec_fixed_auto ("extensions_sig", + &sec.extensions_sig), GNUNET_JSON_spec_end () }; - MHD_RESULT ret; - // Parse the top level json structure + /* Parse the top level json structure */ { enum GNUNET_GenericReturnValue res; @@ -201,159 +223,52 @@ TEH_handler_management_post_extensions ( return MHD_YES; /* failure */ } - // Ensure we have two arrays of the same size - if (! (json_is_array (extensions) && - json_is_array (extensions_sigs)) ) + /* Verify the signature */ { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (top_spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "array expected for extensions and extensions_sigs"); - } - - sec.num_extensions = json_array_size (extensions_sigs); - if (json_array_size (extensions) != sec.num_extensions) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (top_spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "arrays extensions and extensions_sigs are not of the same size"); - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received /management/extensions\n"); - - sec.extensions = GNUNET_new_array (sec.num_extensions, - struct Extension); - sec.extensions_sigs = GNUNET_new_array (sec.num_extensions, - struct TALER_MasterSignatureP); - - // Now parse individual extensions and signatures from those arrays. - for (unsigned int i = 0; i<sec.num_extensions; i++) - { - // 1. parse the extension out of the json - enum GNUNET_GenericReturnValue res; - const struct TALER_Extension *extension; - const char *name; - struct GNUNET_JSON_Specification ext_spec[] = { - GNUNET_JSON_spec_string ("extension", - &name), - GNUNET_JSON_spec_json ("config", - &sec.extensions[i].config), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_array (connection, - extensions, - ext_spec, - i, - -1); - if (GNUNET_SYSERR == res) - { - ret = MHD_NO; /* hard failure */ - goto CLEANUP; - } - if (GNUNET_NO == res) - { - ret = MHD_YES; - goto CLEANUP; - } - - /* 2. Make sure name refers to a supported extension */ - if (GNUNET_OK != TALER_extension_get_by_name (name, - (const struct - TALER_Extension **) - TEH_extensions, - &extension)) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "invalid extension type"); - goto CLEANUP; - } - - sec.extensions[i].type = extension->type; - - /* 3. Extract the signature out of the json array */ - { - enum GNUNET_GenericReturnValue res; - struct GNUNET_JSON_Specification sig_spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &sec.extensions_sigs[i]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_array (connection, - extensions_sigs, - sig_spec, - i, - -1); - if (GNUNET_SYSERR == res) - { - ret = MHD_NO; /* hard failure */ - goto CLEANUP; - } - if (GNUNET_NO == res) - { - ret = MHD_YES; - goto CLEANUP; - } - } - - /* 4. Verify the signature of the config */ - if (GNUNET_OK != config_verify ( - sec.extensions[i].config, + struct TALER_ExtensionManifestsHashP h_manifests; + + if (GNUNET_OK != + TALER_JSON_extensions_manifests_hash (extensions, + &h_manifests) || + GNUNET_OK != + TALER_exchange_offline_extension_manifests_hash_verify ( + &h_manifests, &TEH_master_public_key, - &sec.extensions_sigs[i])) + &sec.extensions_sig)) { - ret = TALER_MHD_reply_with_error ( + return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "invalid signature for extension"); - goto CLEANUP; - } - - /* 5. Make sure the config is sound */ - if (GNUNET_OK != extension->test_config (sec.extensions[i].config)) - { - GNUNET_JSON_parse_free (ext_spec); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "invalid configuration for extension"); - goto CLEANUP; - + "invalid signuture"); } + } - /* We have a validly signed JSON object for the extension. - * Increment its refcount and free the parser for the extension. - */ - json_incref (sec.extensions[i].config); - GNUNET_JSON_parse_free (ext_spec); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received /management/extensions\n"); - } /* for-loop */ + /* Now parse individual extensions and signatures from those objects. */ + if (GNUNET_OK != + verify_extensions_from_json (extensions, &sec)) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "invalid object"); + } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received %u extensions\n", sec.num_extensions); - // now run the transaction to persist the configurations + /* now run the transaction to persist the configurations */ { enum GNUNET_GenericReturnValue res; res = TEH_DB_run_transaction (connection, "set extensions", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &ret, &set_extensions, &sec); @@ -372,14 +287,12 @@ TEH_handler_management_post_extensions ( CLEANUP: for (unsigned int i = 0; i < sec.num_extensions; i++) { - if (NULL != sec.extensions[i].config) + if (NULL != sec.extensions[i].manifest) { - json_decref (sec.extensions[i].config); + json_decref (sec.extensions[i].manifest); } } GNUNET_free (sec.extensions); - GNUNET_free (sec.extensions_sigs); - GNUNET_JSON_parse_free (top_spec); return ret; } diff --git a/src/exchange/taler-exchange-httpd_management_global_fees.c b/src/exchange/taler-exchange-httpd_management_global_fees.c new file mode 100644 index 000000000..8203ddefb --- /dev/null +++ b/src/exchange/taler-exchange-httpd_management_global_fees.c @@ -0,0 +1,261 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2021, 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_management_global_fees.c + * @brief Handle request to add global fee details + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for the #add_fee transaction. + */ +struct AddFeeContext +{ + /** + * Fee's signature affirming the #TALER_SIGNATURE_MASTER_GLOBAL_FEES operation. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Starting period. + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * End of period. + */ + struct GNUNET_TIME_Timestamp end_time; + + /** + * Global fee amounts. + */ + struct TALER_GlobalFeeSet fees; + + /** + * When does an unmerged purse expire? + */ + struct GNUNET_TIME_Relative purse_timeout; + + /** + * When does an account without KYC expire? + */ + struct GNUNET_TIME_Relative kyc_timeout; + + /** + * When does an account history expire? + */ + struct GNUNET_TIME_Relative history_expiration; + + /** + * Number of free purses per account. + */ + uint32_t purse_account_limit; + +}; + + +/** + * Function implementing database transaction to add a fee. Runs the + * transaction logic; IF it returns a non-error code, the transaction logic + * MUST NOT queue a MHD response. IF it returns an hard error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it + * returns the soft error code, the function MAY be called again to retry and + * MUST not queue a MHD response. + * + * @param cls closure with a `struct AddFeeContext` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +add_fee (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AddFeeContext *afc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_GlobalFeeSet fees; + struct GNUNET_TIME_Relative purse_timeout; + struct GNUNET_TIME_Relative history_expiration; + uint32_t purse_account_limit; + + qs = TEH_plugin->lookup_global_fee_by_time ( + TEH_plugin->cls, + afc->start_time, + afc->end_time, + &fees, + &purse_timeout, + &history_expiration, + &purse_account_limit); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup global fee"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) + { + if ( (GNUNET_OK == + TALER_amount_is_valid (&fees.history)) && + (0 == + TALER_global_fee_set_cmp (&fees, + &afc->fees)) ) + { + /* this will trigger the 'success' response */ + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + else + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH, + NULL); + } + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->insert_global_fee ( + TEH_plugin->cls, + afc->start_time, + afc->end_time, + &afc->fees, + afc->purse_timeout, + afc->history_expiration, + afc->purse_account_limit, + &afc->master_sig); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert fee"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_management_post_global_fees ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct AddFeeContext afc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &afc.master_sig), + GNUNET_JSON_spec_timestamp ("fee_start", + &afc.start_time), + GNUNET_JSON_spec_timestamp ("fee_end", + &afc.end_time), + TALER_JSON_spec_amount ("history_fee", + TEH_currency, + &afc.fees.history), + TALER_JSON_spec_amount ("account_fee", + TEH_currency, + &afc.fees.account), + TALER_JSON_spec_amount ("purse_fee", + TEH_currency, + &afc.fees.purse), + GNUNET_JSON_spec_relative_time ("purse_timeout", + &afc.purse_timeout), + GNUNET_JSON_spec_relative_time ("history_expiration", + &afc.history_expiration), + GNUNET_JSON_spec_uint32 ("purse_account_limit", + &afc.purse_account_limit), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_global_fee_verify ( + afc.start_time, + afc.end_time, + &afc.fees, + afc.purse_timeout, + afc.history_expiration, + afc.purse_account_limit, + &TEH_master_public_key, + &afc.master_sig)) + { + /* signature invalid */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID, + NULL); + } + + { + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + + res = TEH_DB_run_transaction (connection, + "add global fee", + TEH_MT_REQUEST_OTHER, + &ret, + &add_fee, + &afc); + if (GNUNET_SYSERR == res) + return ret; + } + TEH_keys_update_states (); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_global_fees.c */ diff --git a/src/exchange/taler-exchange-httpd_management_partners.c b/src/exchange/taler-exchange-httpd_management_partners.c new file mode 100644 index 000000000..fc8a4207d --- /dev/null +++ b/src/exchange/taler-exchange-httpd_management_partners.c @@ -0,0 +1,132 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_management_partners.c + * @brief Handle request to add exchange partner + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +MHD_RESULT +TEH_handler_management_partners ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct TALER_MasterPublicKeyP partner_pub; + struct GNUNET_TIME_Timestamp start_date; + struct GNUNET_TIME_Timestamp end_date; + struct GNUNET_TIME_Relative wad_frequency; + struct TALER_Amount wad_fee; + const char *partner_base_url; + struct TALER_MasterSignatureP master_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("partner_pub", + &partner_pub), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + TALER_JSON_spec_web_url ("partner_base_url", + &partner_base_url), + TALER_JSON_spec_amount ("wad_fee", + TEH_currency, + &wad_fee), + GNUNET_JSON_spec_timestamp ("start_date", + &start_date), + GNUNET_JSON_spec_timestamp ("end_date", + &end_date), + GNUNET_JSON_spec_relative_time ("wad_frequency", + &wad_frequency), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_partner_details_verify ( + &partner_pub, + start_date, + end_date, + wad_frequency, + &wad_fee, + partner_base_url, + &TEH_master_public_key, + &master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID, + NULL); + } + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->insert_partner (TEH_plugin->cls, + &partner_pub, + start_date, + end_date, + wad_frequency, + &wad_fee, + partner_base_url, + &master_sig); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "add_partner"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* FIXME-#7271: check for idempotency! */ + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT, + NULL); + } + } + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_partners.c */ diff --git a/src/exchange/taler-exchange-httpd_management_post_keys.c b/src/exchange/taler-exchange-httpd_management_post_keys.c index f0c3f1f39..f91f24c41 100644 --- a/src/exchange/taler-exchange-httpd_management_post_keys.c +++ b/src/exchange/taler-exchange-httpd_management_post_keys.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020, 2021 Taler Systems SA + 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 Affero General Public License as published by the Free Software @@ -40,13 +40,23 @@ struct DenomSig /** * Hash of a denomination public key. */ - struct TALER_DenominationHash h_denom_pub; + struct TALER_DenominationHashP h_denom_pub; /** * Master signature for the @e h_denom_pub. */ struct TALER_MasterSignatureP master_sig; + /** + * Fee structure for this key, as per our configuration. + */ + struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; + + /** + * The full public key. + */ + struct TALER_DenominationPublicKey denom_pub; + }; @@ -65,6 +75,11 @@ struct SigningSig */ struct TALER_MasterSignatureP master_sig; + /** + * Our meta data on this key. + */ + struct TALER_EXCHANGEDB_SignkeyMetaData meta; + }; @@ -85,6 +100,11 @@ struct AddKeysContext struct SigningSig *s_sigs; /** + * Our key state. + */ + struct TEH_KeyStateHandle *ksh; + + /** * Length of the d_sigs array. */ unsigned int nd_sigs; @@ -123,14 +143,9 @@ add_keys (void *cls, { struct DenomSig *d = &akc->d_sigs[i]; enum GNUNET_DB_QueryStatus qs; - bool is_active = false; struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; - struct TALER_DenominationPublicKey denom_pub; /* For idempotency, check if the key is already active */ - memset (&denom_pub, - 0, - sizeof (denom_pub)); qs = TEH_plugin->lookup_denomination_key ( TEH_plugin->cls, &d->h_denom_pub, @@ -146,78 +161,21 @@ add_keys (void *cls, "lookup denomination key"); return qs; } - if (0 == qs) - { - enum GNUNET_GenericReturnValue rv; - - rv = TEH_keys_load_fees (&d->h_denom_pub, - &denom_pub, - &meta); - switch (rv) - { - case GNUNET_SYSERR: - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, - GNUNET_h2s (&d->h_denom_pub.hash)); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_NO: - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, - GNUNET_h2s (&d->h_denom_pub.hash)); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_OK: - break; - } - } - else - { - is_active = true; - } - - /* check signature is valid */ - if (GNUNET_OK != - TALER_exchange_offline_denom_validity_verify ( - &d->h_denom_pub, - meta.start, - meta.expire_withdraw, - meta.expire_deposit, - meta.expire_legal, - &meta.value, - &meta.fee_withdraw, - &meta.fee_deposit, - &meta.fee_refresh, - &meta.fee_refund, - &TEH_master_public_key, - &d->master_sig)) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID, - GNUNET_h2s (&d->h_denom_pub.hash)); - if (! is_active) - TALER_denom_pub_free (&denom_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (is_active) + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { + /* FIXME: assert meta === d->meta might be good */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Denomination key %s already active, skipping\n", GNUNET_h2s (&d->h_denom_pub.hash)); continue; /* skip, already known */ } + qs = TEH_plugin->add_denomination_key ( TEH_plugin->cls, &d->h_denom_pub, - &denom_pub, - &meta, + &d->denom_pub, + &d->meta, &d->master_sig); - TALER_denom_pub_free (&denom_pub); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -239,7 +197,6 @@ add_keys (void *cls, { struct SigningSig *s = &akc->s_sigs[i]; enum GNUNET_DB_QueryStatus qs; - bool is_active = false; struct TALER_EXCHANGEDB_SignkeyMetaData meta; qs = TEH_plugin->lookup_signing_key ( @@ -257,46 +214,9 @@ add_keys (void *cls, "lookup signing key"); return qs; } - if (0 == qs) - { - if (GNUNET_OK != - TEH_keys_get_timing (&s->exchange_pub, - &meta)) - { - /* For idempotency, check if the key is already active */ - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN, - TALER_B2S (&s->exchange_pub)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - else - { - is_active = true; /* if we pass, it's active! */ - } - - /* check signature is valid */ - if (GNUNET_OK != - TALER_exchange_offline_signkey_validity_verify ( - &s->exchange_pub, - meta.start, - meta.expire_sign, - meta.expire_legal, - &TEH_master_public_key, - &s->master_sig)) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID, - TALER_B2S (&s->exchange_pub)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (is_active) + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { + /* FIXME: assert meta === d->meta might be good */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Signing key %s already active, skipping\n", TALER_B2S (&s->exchange_pub)); @@ -305,7 +225,7 @@ add_keys (void *cls, qs = TEH_plugin->activate_signing_key ( TEH_plugin->cls, &s->exchange_pub, - &meta, + &s->meta, &s->master_sig); if (qs < 0) { @@ -327,22 +247,40 @@ add_keys (void *cls, } +/** + * Clean up state in @a akc, but do not free @a akc itself + * + * @param[in,out] akc state to clean up + */ +static void +cleanup_akc (struct AddKeysContext *akc) +{ + for (unsigned int i = 0; i<akc->nd_sigs; i++) + { + struct DenomSig *d = &akc->d_sigs[i]; + + TALER_denom_pub_free (&d->denom_pub); + } + GNUNET_free (akc->d_sigs); + GNUNET_free (akc->s_sigs); +} + + MHD_RESULT TEH_handler_management_post_keys ( struct MHD_Connection *connection, const json_t *root) { - struct AddKeysContext akc; - json_t *denom_sigs; - json_t *signkey_sigs; + struct AddKeysContext akc = { 0 }; + const json_t *denom_sigs; + const json_t *signkey_sigs; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("denom_sigs", - &denom_sigs), - GNUNET_JSON_spec_json ("signkey_sigs", - &signkey_sigs), + GNUNET_JSON_spec_array_const ("denom_sigs", + &denom_sigs), + GNUNET_JSON_spec_array_const ("signkey_sigs", + &signkey_sigs), GNUNET_JSON_spec_end () }; - bool ok; MHD_RESULT ret; { @@ -356,23 +294,23 @@ TEH_handler_management_post_keys ( if (GNUNET_NO == res) return MHD_YES; /* failure */ } - if (! (json_is_array (denom_sigs) && - json_is_array (signkey_sigs)) ) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received POST /management/keys request\n"); + + akc.ksh = TEH_keys_get_state_for_management_only (); /* may start its own transaction, thus must be done here, before we run ours! */ + if (NULL == akc.ksh) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "array expected for denom_sigs and signkey_sigs"); + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + "no key state (not even for management)"); } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received /management/keys\n"); + akc.nd_sigs = json_array_size (denom_sigs); akc.d_sigs = GNUNET_new_array (akc.nd_sigs, struct DenomSig); - ok = true; for (unsigned int i = 0; i<akc.nd_sigs; i++) { struct DenomSig *d = &akc.d_sigs[i]; @@ -389,27 +327,64 @@ TEH_handler_management_post_keys ( json_array_get (denom_sigs, i), ispec); - if (GNUNET_SYSERR == res) + if (GNUNET_OK != res) { - ret = MHD_NO; /* hard failure */ - ok = false; - break; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failure to handle /management/keys\n"); + cleanup_akc (&akc); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } - if (GNUNET_NO == res) + + res = TEH_keys_load_fees (akc.ksh, + &d->h_denom_pub, + &d->denom_pub, + &d->meta); + switch (res) { - ret = MHD_YES; - ok = false; + case GNUNET_SYSERR: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + GNUNET_h2s (&d->h_denom_pub.hash)); + cleanup_akc (&akc); + return ret; + case GNUNET_NO: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + GNUNET_h2s (&d->h_denom_pub.hash)); + cleanup_akc (&akc); + return ret; + case GNUNET_OK: break; } + /* check signature is valid */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_denom_validity_verify ( + &d->h_denom_pub, + d->meta.start, + d->meta.expire_withdraw, + d->meta.expire_deposit, + d->meta.expire_legal, + &d->meta.value, + &d->meta.fees, + &TEH_master_public_key, + &d->master_sig)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID, + GNUNET_h2s (&d->h_denom_pub.hash)); + cleanup_akc (&akc); + return ret; + } } - if (! ok) - { - GNUNET_free (akc.d_sigs); - GNUNET_JSON_parse_free (spec); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failure to handle /management/keys\n"); - return ret; - } + akc.ns_sigs = json_array_size (signkey_sigs); akc.s_sigs = GNUNET_new_array (akc.ns_sigs, struct SigningSig); @@ -429,27 +404,58 @@ TEH_handler_management_post_keys ( json_array_get (signkey_sigs, i), ispec); - if (GNUNET_SYSERR == res) + if (GNUNET_OK != res) { - ret = MHD_NO; /* hard failure */ - ok = false; - break; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failure to handle /management/keys\n"); + cleanup_akc (&akc); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } - if (GNUNET_NO == res) + res = TEH_keys_get_timing (&s->exchange_pub, + &s->meta); + switch (res) { - ret = MHD_YES; - ok = false; + case GNUNET_SYSERR: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + TALER_B2S (&s->exchange_pub)); + cleanup_akc (&akc); + return ret; + case GNUNET_NO: + /* For idempotency, check if the key is already active */ + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN, + TALER_B2S (&s->exchange_pub)); + cleanup_akc (&akc); + return ret; + case GNUNET_OK: break; } - } - if (! ok) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failure to handle /management/keys\n"); - GNUNET_free (akc.d_sigs); - GNUNET_free (akc.s_sigs); - GNUNET_JSON_parse_free (spec); - return ret; + + /* check signature is valid */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_signkey_validity_verify ( + &s->exchange_pub, + s->meta.start, + s->meta.expire_sign, + s->meta.expire_legal, + &TEH_master_public_key, + &s->master_sig)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID, + TALER_B2S (&s->exchange_pub)); + cleanup_akc (&akc); + return ret; + } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received %u denomination and %u signing key signatures\n", @@ -460,13 +466,11 @@ TEH_handler_management_post_keys ( res = TEH_DB_run_transaction (connection, "add keys", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &ret, &add_keys, &akc); - GNUNET_free (akc.d_sigs); - GNUNET_free (akc.s_sigs); - GNUNET_JSON_parse_free (spec); + cleanup_akc (&akc); if (GNUNET_SYSERR == res) return ret; } diff --git a/src/exchange/taler-exchange-httpd_management_signkey_EP_revoke.c b/src/exchange/taler-exchange-httpd_management_signkey_EP_revoke.c index 0d6317b47..6c3683fc5 100644 --- a/src/exchange/taler-exchange-httpd_management_signkey_EP_revoke.c +++ b/src/exchange/taler-exchange-httpd_management_signkey_EP_revoke.c @@ -56,6 +56,7 @@ TEH_handler_management_signkeys_EP_revoke ( if (GNUNET_NO == res) return MHD_YES; /* failure */ } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_exchange_offline_signkey_revoke_verify (exchange_pub, &TEH_master_public_key, diff --git a/src/exchange/taler-exchange-httpd_management_wire_disable.c b/src/exchange/taler-exchange-httpd_management_wire_disable.c index d776bc353..e0b8a3de8 100644 --- a/src/exchange/taler-exchange-httpd_management_wire_disable.c +++ b/src/exchange/taler-exchange-httpd_management_wire_disable.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020 Taler Systems SA + 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 Affero General Public License as published by the Free Software @@ -28,7 +28,7 @@ #include "taler_mhd_lib.h" #include "taler-exchange-httpd_management.h" #include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_wire.h" +#include "taler-exchange-httpd_keys.h" /** @@ -103,7 +103,7 @@ del_wire (void *cls, NULL); return GNUNET_DB_STATUS_HARD_ERROR; } - if (0 == qs) + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { *mhd_ret = TALER_MHD_reply_with_error ( connection, @@ -114,7 +114,13 @@ del_wire (void *cls, } qs = TEH_plugin->update_wire (TEH_plugin->cls, awc->payto_uri, + NULL, + NULL, + NULL, awc->validity_end, + NULL, + NULL, + 0, false); if (qs < 0) { @@ -140,8 +146,8 @@ TEH_handler_management_post_wire_disable ( struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("master_sig_del", &awc.master_sig), - GNUNET_JSON_spec_string ("payto_uri", - &awc.payto_uri), + TALER_JSON_spec_payto_uri ("payto_uri", + &awc.payto_uri), GNUNET_JSON_spec_timestamp ("validity_end", &awc.validity_end), GNUNET_JSON_spec_end () @@ -179,7 +185,7 @@ TEH_handler_management_post_wire_disable ( res = TEH_DB_run_transaction (connection, "del wire", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &ret, &del_wire, &awc); diff --git a/src/exchange/taler-exchange-httpd_management_wire_enable.c b/src/exchange/taler-exchange-httpd_management_wire_enable.c index 56828eb5e..472e19d3e 100644 --- a/src/exchange/taler-exchange-httpd_management_wire_enable.c +++ b/src/exchange/taler-exchange-httpd_management_wire_enable.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020 Taler Systems SA + Copyright (C) 2020-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -29,7 +29,7 @@ #include "taler_signatures.h" #include "taler-exchange-httpd_management.h" #include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_wire.h" +#include "taler-exchange-httpd_keys.h" /** @@ -55,10 +55,35 @@ struct AddWireContext const char *payto_uri; /** + * (optional) address of a conversion service for this account. + */ + const char *conversion_url; + + /** + * Restrictions imposed when crediting this account. + */ + const json_t *credit_restrictions; + + /** + * Restrictions imposed when debiting this account. + */ + const json_t *debit_restrictions; + + /** * Timestamp for checking against replay attacks. */ struct GNUNET_TIME_Timestamp validity_start; + /** + * Label to use for this bank. Default is empty. + */ + const char *bank_label; + + /** + * Priority of the bank in the list. Default 0. + */ + int64_t priority; + }; @@ -111,15 +136,26 @@ add_wire (void *cls, NULL); return GNUNET_DB_STATUS_HARD_ERROR; } - if (0 == qs) + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) qs = TEH_plugin->insert_wire (TEH_plugin->cls, awc->payto_uri, + awc->conversion_url, + awc->debit_restrictions, + awc->credit_restrictions, awc->validity_start, - &awc->master_sig_wire); + &awc->master_sig_wire, + awc->bank_label, + awc->priority); else qs = TEH_plugin->update_wire (TEH_plugin->cls, awc->payto_uri, + awc->conversion_url, + awc->debit_restrictions, + awc->credit_restrictions, awc->validity_start, + &awc->master_sig_wire, + awc->bank_label, + awc->priority, true); if (qs < 0) { @@ -141,16 +177,34 @@ TEH_handler_management_post_wire ( struct MHD_Connection *connection, const json_t *root) { - struct AddWireContext awc; + struct AddWireContext awc = { + .conversion_url = NULL + }; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("master_sig_wire", &awc.master_sig_wire), GNUNET_JSON_spec_fixed_auto ("master_sig_add", &awc.master_sig_add), - GNUNET_JSON_spec_string ("payto_uri", - &awc.payto_uri), + TALER_JSON_spec_payto_uri ("payto_uri", + &awc.payto_uri), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("conversion_url", + &awc.conversion_url), + NULL), + GNUNET_JSON_spec_array_const ("credit_restrictions", + &awc.credit_restrictions), + GNUNET_JSON_spec_array_const ("debit_restrictions", + &awc.debit_restrictions), GNUNET_JSON_spec_timestamp ("validity_start", &awc.validity_start), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("bank_label", + &awc.bank_label), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_int64 ("priority", + &awc.priority), + NULL), GNUNET_JSON_spec_end () }; @@ -165,25 +219,55 @@ TEH_handler_management_post_wire ( if (GNUNET_NO == res) return MHD_YES; /* failure */ } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + { + char *msg = TALER_payto_validate (awc.payto_uri); + + if (NULL != msg) + { + MHD_RESULT ret; + + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + msg); + GNUNET_JSON_parse_free (spec); + GNUNET_free (msg); + return ret; + } + } if (GNUNET_OK != - TALER_exchange_offline_wire_add_verify (awc.payto_uri, - awc.validity_start, - &TEH_master_public_key, - &awc.master_sig_add)) + TALER_exchange_offline_wire_add_verify ( + awc.payto_uri, + awc.conversion_url, + awc.debit_restrictions, + awc.credit_restrictions, + awc.validity_start, + &TEH_master_public_key, + &awc.master_sig_add)) { GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID, NULL); } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != - TALER_exchange_wire_signature_check (awc.payto_uri, - &TEH_master_public_key, - &awc.master_sig_wire)) + TALER_exchange_wire_signature_check ( + awc.payto_uri, + awc.conversion_url, + awc.debit_restrictions, + awc.credit_restrictions, + &TEH_master_public_key, + &awc.master_sig_wire)) { GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, @@ -199,6 +283,7 @@ TEH_handler_management_post_wire ( GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "payto:// URI `%s' is malformed\n", awc.payto_uri); + GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, @@ -214,10 +299,11 @@ TEH_handler_management_post_wire ( res = TEH_DB_run_transaction (connection, "add wire", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &ret, &add_wire, &awc); + GNUNET_JSON_parse_free (spec); if (GNUNET_SYSERR == res) return ret; } diff --git a/src/exchange/taler-exchange-httpd_management_wire_fees.c b/src/exchange/taler-exchange-httpd_management_wire_fees.c index c14500e8d..cb87592a5 100644 --- a/src/exchange/taler-exchange-httpd_management_wire_fees.c +++ b/src/exchange/taler-exchange-httpd_management_wire_fees.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020, 2021 Taler Systems SA + Copyright (C) 2020, 2021, 2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -29,7 +29,7 @@ #include "taler_signatures.h" #include "taler-exchange-httpd_management.h" #include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_wire.h" +#include "taler-exchange-httpd_keys.h" /** @@ -58,14 +58,9 @@ struct AddFeeContext struct GNUNET_TIME_Timestamp end_time; /** - * Wire fee amount. + * Wire fee amounts. */ - struct TALER_Amount wire_fee; - - /** - * Closing fee amount. - */ - struct TALER_Amount closing_fee; + struct TALER_WireFeeSet fees; }; @@ -91,16 +86,14 @@ add_fee (void *cls, { struct AddFeeContext *afc = cls; enum GNUNET_DB_QueryStatus qs; - struct TALER_Amount wire_fee; - struct TALER_Amount closing_fee; + struct TALER_WireFeeSet fees; qs = TEH_plugin->lookup_wire_fee_by_time ( TEH_plugin->cls, afc->wire_method, afc->start_time, afc->end_time, - &wire_fee, - &closing_fee); + &fees); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -115,13 +108,10 @@ add_fee (void *cls, if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) { if ( (GNUNET_OK == - TALER_amount_is_valid (&wire_fee)) && - (0 == - TALER_amount_cmp (&wire_fee, - &afc->wire_fee)) && + TALER_amount_is_valid (&fees.wire)) && (0 == - TALER_amount_cmp (&closing_fee, - &afc->closing_fee)) ) + TALER_wire_fee_set_cmp (&fees, + &afc->fees)) ) { /* this will trigger the 'success' response */ return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; @@ -142,8 +132,7 @@ add_fee (void *cls, afc->wire_method, afc->start_time, afc->end_time, - &afc->wire_fee, - &afc->closing_fee, + &afc->fees, &afc->master_sig); if (qs < 0) { @@ -175,12 +164,12 @@ TEH_handler_management_post_wire_fees ( &afc.start_time), GNUNET_JSON_spec_timestamp ("fee_end", &afc.end_time), - TALER_JSON_spec_amount ("closing_fee", - TEH_currency, - &afc.closing_fee), TALER_JSON_spec_amount ("wire_fee", TEH_currency, - &afc.wire_fee), + &afc.fees.wire), + TALER_JSON_spec_amount ("closing_fee", + TEH_currency, + &afc.fees.closing), GNUNET_JSON_spec_end () }; @@ -196,13 +185,13 @@ TEH_handler_management_post_wire_fees ( return MHD_YES; /* failure */ } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_exchange_offline_wire_fee_verify ( afc.wire_method, afc.start_time, afc.end_time, - &afc.wire_fee, - &afc.closing_fee, + &afc.fees, &TEH_master_public_key, &afc.master_sig)) { @@ -221,7 +210,7 @@ TEH_handler_management_post_wire_fees ( res = TEH_DB_run_transaction (connection, "add wire fee", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &ret, &add_fee, &afc); diff --git a/src/exchange/taler-exchange-httpd_melt.c b/src/exchange/taler-exchange-httpd_melt.c index 54f1385d7..b31078f00 100644 --- a/src/exchange/taler-exchange-httpd_melt.c +++ b/src/exchange/taler-exchange-httpd_melt.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 Affero General Public License as published by the Free Software @@ -48,18 +48,15 @@ reply_melt_success (struct MHD_Connection *connection, { struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; - struct TALER_RefreshMeltConfirmationPS body = { - .purpose.size = htonl (sizeof (body)), - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_MELT), - .rc = *rc, - .noreveal_index = htonl (noreveal_index) - }; enum TALER_ErrorCode ec; if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign (&body, - &pub, - &sig))) + (ec = TALER_exchange_online_melt_confirmation_sign ( + &TEH_keys_exchange_sign_, + rc, + noreveal_index, + &pub, + &sig))) { return TALER_MHD_reply_with_ec (connection, ec, @@ -105,6 +102,11 @@ struct MeltContext struct TALER_Amount coin_refresh_fee; /** + * Refresh master secret, if any of the fresh denominations use CS. + */ + struct TALER_RefreshMasterSecretP rms; + + /** * Set to true if this coin's denomination was revoked and the operation * is thus only allowed for zombie coins where the transaction * history includes a #TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP. @@ -117,6 +119,10 @@ struct MeltContext */ bool coin_is_dirty; + /** + * True if @e rms is missing. + */ + bool no_rms; }; @@ -155,6 +161,9 @@ melt_transaction (void *cls, if (0 > (qs = TEH_plugin->do_melt (TEH_plugin->cls, + rmc->no_rms + ? NULL + : &rmc->rms, &rmc->refresh_session, rmc->known_coin_id, &rmc->zombie_required, @@ -174,7 +183,6 @@ melt_transaction (void *cls, if (rmc->zombie_required) { GNUNET_break_op (0); - TEH_plugin->rollback (TEH_plugin->cls); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, @@ -183,15 +191,17 @@ melt_transaction (void *cls, } if (! balance_ok) { - TEH_plugin->rollback (TEH_plugin->cls); + GNUNET_break_op (0); *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &rmc->refresh_session.coin.denom_pub_hash, &rmc->refresh_session.coin.coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } /* All good, commit, final response will be generated by caller */ + TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT]++; return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } @@ -224,12 +234,23 @@ database_melt (struct MHD_Connection *connection, MHD_RESULT mhd_ret = MHD_NO; enum GNUNET_DB_QueryStatus qs; - qs = TEH_make_coin_known (&rmc->refresh_session.coin, - connection, - &rmc->known_coin_id, - &mhd_ret); - /* no transaction => no serialization failures should be possible */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++) + { + qs = TEH_make_coin_known (&rmc->refresh_session.coin, + connection, + &rmc->known_coin_id, + &mhd_ret); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "make_coin_known"); + } if (qs < 0) return mhd_ret; } @@ -241,7 +262,7 @@ database_melt (struct MHD_Connection *connection, if (GNUNET_OK != TEH_DB_run_transaction (connection, "run melt", - TEH_MT_MELT, + TEH_MT_REQUEST_MELT, &mhd_ret, &melt_transaction, rmc)) @@ -267,7 +288,7 @@ static MHD_RESULT check_melt_valid (struct MHD_Connection *connection, struct MeltContext *rmc) { - /* Baseline: check if deposits/refreshs are generally + /* Baseline: check if deposits/refreshes are generally simply still allowed for this denomination */ struct TEH_DenominationKey *dk; MHD_RESULT mret; @@ -278,6 +299,7 @@ check_melt_valid (struct MHD_Connection *connection, &mret); if (NULL == dk) return mret; + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_legal.abs_time)) { /* Way too late now, even zombies have expired */ @@ -287,6 +309,7 @@ check_melt_valid (struct MHD_Connection *connection, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "MELT"); } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) { /* This denomination is not yet valid */ @@ -297,8 +320,13 @@ check_melt_valid (struct MHD_Connection *connection, "MELT"); } - rmc->coin_refresh_fee = dk->meta.fee_refresh; + rmc->coin_refresh_fee = dk->meta.fees.refresh; rmc->coin_value = dk->meta.value; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Melted coin's denomination is worth %s\n", + TALER_amount2s (&dk->meta.value)); + /* sanity-check that "total melt amount > melt fee" */ if (0 < TALER_amount_cmp (&rmc->coin_refresh_fee, @@ -310,7 +338,17 @@ check_melt_valid (struct MHD_Connection *connection, TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, NULL); } - + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } if (GNUNET_OK != TALER_test_coin_valid (&rmc->refresh_session.coin, &dk->denom_pub)) @@ -323,11 +361,13 @@ check_melt_valid (struct MHD_Connection *connection, } /* verify signature of coin for melt operation */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_melt_verify (&rmc->refresh_session.amount_with_fee, &rmc->coin_refresh_fee, &rmc->refresh_session.rc, &rmc->refresh_session.coin.denom_pub_hash, + &rmc->refresh_session.coin.h_age_commitment, &rmc->refresh_session.coin.coin_pub, &rmc->refresh_session.coin_sig)) { @@ -341,7 +381,7 @@ check_melt_valid (struct MHD_Connection *connection, if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) { /* We are past deposit expiration time, but maybe this is a zombie? */ - struct TALER_DenominationHash denom_hash; + struct TALER_DenominationHashP denom_hash; enum GNUNET_DB_QueryStatus qs; /* Check that the coin is dirty (we have seen it before), as we will @@ -403,6 +443,10 @@ TEH_handler_melt (struct MHD_Connection *connection, &rmc.refresh_session.coin.denom_sig), GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", &rmc.refresh_session.coin.denom_pub_hash), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("age_commitment_hash", + &rmc.refresh_session.coin.h_age_commitment), + &rmc.refresh_session.coin.no_age_commitment), GNUNET_JSON_spec_fixed_auto ("confirm_sig", &rmc.refresh_session.coin_sig), TALER_JSON_spec_amount ("value_with_fee", @@ -410,12 +454,14 @@ TEH_handler_melt (struct MHD_Connection *connection, &rmc.refresh_session.amount_with_fee), GNUNET_JSON_spec_fixed_auto ("rc", &rmc.refresh_session.rc), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("rms", + &rmc.rms), + &rmc.no_rms), GNUNET_JSON_spec_end () }; - memset (&rmc, - 0, - sizeof (rmc)); + memset (&rmc, 0, sizeof (rmc)); rmc.refresh_session.coin.coin_pub = *coin_pub; { diff --git a/src/exchange/taler-exchange-httpd_melt.h b/src/exchange/taler-exchange-httpd_melt.h index 0edaf2475..b15fd07c7 100644 --- a/src/exchange/taler-exchange-httpd_melt.h +++ b/src/exchange/taler-exchange-httpd_melt.h @@ -30,10 +30,10 @@ /** * Handle a "/coins/$COIN_PUB/melt" request. Parses the request into the JSON - * components and then hands things of to #check_for_denomination_key() to + * components and then hands things of to #check_melt_valid() to * validate the melted coins, the signature and execute the melt using - * handle_melt(). - + * melt_transaction(). + * * @param connection the MHD connection to handle * @param coin_pub public key of the coin * @param root uploaded JSON data diff --git a/src/exchange/taler-exchange-httpd_metrics.c b/src/exchange/taler-exchange-httpd_metrics.c index 8c8cd343a..1542801fe 100644 --- a/src/exchange/taler-exchange-httpd_metrics.c +++ b/src/exchange/taler-exchange-httpd_metrics.c @@ -29,9 +29,19 @@ #include <jansson.h> -unsigned long long TEH_METRICS_num_requests[TEH_MT_COUNT]; +unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; -unsigned long long TEH_METRICS_num_conflict[TEH_MT_COUNT]; +unsigned long long TEH_METRICS_batch_withdraw_num_coins; + +unsigned long long TEH_METRICS_num_conflict[TEH_MT_REQUEST_COUNT]; + +unsigned long long TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_COUNT]; + +unsigned long long TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_COUNT]; + +unsigned long long TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_COUNT]; + +unsigned long long TEH_METRICS_num_success[TEH_MT_SUCCESS_COUNT]; MHD_RESULT @@ -44,6 +54,11 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, (void) args; GNUNET_asprintf (&reply, + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" "# HELP taler_exchange_serialization_failures " " number of database serialization errors by type\n" "# TYPE taler_exchange_serialization_failures counter\n" @@ -57,23 +72,85 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, "taler_exchange_received_requests{type=\"%s\"} %llu\n" "taler_exchange_received_requests{type=\"%s\"} %llu\n" "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n", + "taler_exchange_received_requests{type=\"%s\"} %llu\n" + "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" +#if NOT_YET_IMPLEMENTED + "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" + "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" +#endif + "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" + "# HELP taler_exchange_num_signatures " + " number of signatures created by cipher\n" + "# TYPE taler_exchange_num_signatures counter\n" + "taler_exchange_num_signatures{type=\"%s\"} %llu\n" + "taler_exchange_num_signatures{type=\"%s\"} %llu\n" + "taler_exchange_num_signatures{type=\"%s\"} %llu\n" + "# HELP taler_exchange_num_signature_verifications " + " number of signatures verified by cipher\n" + "# TYPE taler_exchange_num_signature_verifications counter\n" + "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" + "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" + "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" + "# HELP taler_exchange_num_keyexchanges " + " number of key exchanges done by cipher\n" + "# TYPE taler_exchange_num_keyexchanges counter\n" + "taler_exchange_num_keyexchanges{type=\"%s\"} %llu\n" + "# HELP taler_exchange_batch_withdraw_num_coins " + " number of coins withdrawn in a batch-withdraw request\n" + "# TYPE taler_exchange_batch_withdraw_num_coins counter\n" + "taler_exchange_batch_withdraw_num_coins{} %llu\n", + "deposit", + TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT], + "withdraw", + TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW], + "batch-withdraw", + TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW], + "melt", + TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT], + "refresh-reveal", + TEH_METRICS_num_success[TEH_MT_SUCCESS_REFRESH_REVEAL], "other", - TEH_METRICS_num_conflict[TEH_MT_OTHER], + TEH_METRICS_num_conflict[TEH_MT_REQUEST_OTHER], "deposit", - TEH_METRICS_num_conflict[TEH_MT_DEPOSIT], + TEH_METRICS_num_conflict[TEH_MT_REQUEST_DEPOSIT], "withdraw", - TEH_METRICS_num_conflict[TEH_MT_WITHDRAW], + TEH_METRICS_num_conflict[TEH_MT_REQUEST_WITHDRAW], "melt", - TEH_METRICS_num_conflict[TEH_MT_MELT], + TEH_METRICS_num_conflict[TEH_MT_REQUEST_MELT], "other", - TEH_METRICS_num_requests[TEH_MT_OTHER], + TEH_METRICS_num_requests[TEH_MT_REQUEST_OTHER], "deposit", - TEH_METRICS_num_requests[TEH_MT_DEPOSIT], + TEH_METRICS_num_requests[TEH_MT_REQUEST_DEPOSIT], "withdraw", - TEH_METRICS_num_requests[TEH_MT_WITHDRAW], + TEH_METRICS_num_requests[TEH_MT_REQUEST_WITHDRAW], + "melt", + TEH_METRICS_num_requests[TEH_MT_REQUEST_MELT], + "withdraw", + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW], +#if NOT_YET_IMPLEMENTED + "deposit", + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT], "melt", - TEH_METRICS_num_requests[TEH_MT_MELT]); + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_MELT], +#endif + "batch-withdraw", + TEH_METRICS_num_requests[ + TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW], + "rsa", + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA], + "cs", + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_CS], + "eddsa", + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_EDDSA], + "rsa", + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA], + "cs", + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS], + "eddsa", + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA], + "ecdh", + TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_ECDH], + TEH_METRICS_batch_withdraw_num_coins); resp = MHD_create_response_from_buffer (strlen (reply), reply, MHD_RESPMEM_MUST_FREE); diff --git a/src/exchange/taler-exchange-httpd_metrics.h b/src/exchange/taler-exchange-httpd_metrics.h index 55e5372a7..318113c1f 100644 --- a/src/exchange/taler-exchange-httpd_metrics.h +++ b/src/exchange/taler-exchange-httpd_metrics.h @@ -29,27 +29,97 @@ /** * Request types for which we collect metrics. */ -enum TEH_MetricType +enum TEH_MetricTypeRequest { - TEH_MT_OTHER = 0, - TEH_MT_DEPOSIT = 1, - TEH_MT_WITHDRAW = 2, - TEH_MT_MELT = 3, - TEH_MT_COUNT = 4 /* MUST BE LAST! */ + TEH_MT_REQUEST_OTHER = 0, + TEH_MT_REQUEST_DEPOSIT = 1, + TEH_MT_REQUEST_WITHDRAW = 2, + TEH_MT_REQUEST_AGE_WITHDRAW = 3, + TEH_MT_REQUEST_MELT = 4, + TEH_MT_REQUEST_PURSE_CREATE = 5, + TEH_MT_REQUEST_PURSE_MERGE = 6, + TEH_MT_REQUEST_RESERVE_PURSE = 7, + TEH_MT_REQUEST_PURSE_DEPOSIT = 8, + TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT = 9, + TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW = 10, + TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW = 11, + TEH_MT_REQUEST_IDEMPOTENT_MELT = 12, + TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW = 13, + TEH_MT_REQUEST_BATCH_DEPOSIT = 14, + TEH_MT_REQUEST_POLICY_FULFILLMENT = 15, + TEH_MT_REQUEST_COUNT = 16 /* MUST BE LAST! */ }; +/** + * Success types for which we collect metrics. + */ +enum TEH_MetricTypeSuccess +{ + TEH_MT_SUCCESS_DEPOSIT = 0, + TEH_MT_SUCCESS_WITHDRAW = 1, + TEH_MT_SUCCESS_AGE_WITHDRAW = 2, + TEH_MT_SUCCESS_BATCH_WITHDRAW = 3, + TEH_MT_SUCCESS_MELT = 4, + TEH_MT_SUCCESS_REFRESH_REVEAL = 5, + TEH_MT_SUCCESS_AGE_WITHDRAW_REVEAL = 6, + TEH_MT_SUCCESS_COUNT = 7 /* MUST BE LAST! */ +}; + +/** + * Cipher types for which we collect signature metrics. + */ +enum TEH_MetricTypeSignature +{ + TEH_MT_SIGNATURE_RSA = 0, + TEH_MT_SIGNATURE_CS = 1, + TEH_MT_SIGNATURE_EDDSA = 2, + TEH_MT_SIGNATURE_COUNT = 3 +}; + +/** + * Cipher types for which we collect key exchange metrics. + */ +enum TEH_MetricTypeKeyX +{ + TEH_MT_KEYX_ECDH = 0, + TEH_MT_KEYX_COUNT = 1 +}; /** * Number of requests handled of the respective type. */ -extern unsigned long long TEH_METRICS_num_requests[TEH_MT_COUNT]; +extern unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; + +/** + * Number of successful requests handled of the respective type. + */ +extern unsigned long long TEH_METRICS_num_success[TEH_MT_SUCCESS_COUNT]; + +/** + * Number of coins withdrawn in a batch-withdraw request + */ +extern unsigned long long TEH_METRICS_batch_withdraw_num_coins; /** * Number of serialization errors encountered when * handling requests of the respective type. */ -extern unsigned long long TEH_METRICS_num_conflict[TEH_MT_COUNT]; +extern unsigned long long TEH_METRICS_num_conflict[TEH_MT_REQUEST_COUNT]; + +/** + * Number of signatures created by the respective cipher. + */ +extern unsigned long long TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_COUNT]; +/** + * Number of signatures verified by the respective cipher. + */ +extern unsigned long long TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_COUNT]; + +/** + * Number of key exchanges done with the respective cipher. + */ +extern unsigned long long TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_COUNT]; /** * Handle a "/metrics" request. diff --git a/src/exchange/taler-exchange-httpd_mhd.h b/src/exchange/taler-exchange-httpd_mhd.h index 270b0539a..563975beb 100644 --- a/src/exchange/taler-exchange-httpd_mhd.h +++ b/src/exchange/taler-exchange-httpd_mhd.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2020 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 Affero General Public License as published by the Free Software diff --git a/src/exchange/taler-exchange-httpd_purses_create.c b/src/exchange/taler-exchange-httpd_purses_create.c new file mode 100644 index 000000000..2de9468fe --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_create.c @@ -0,0 +1,651 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_create.c + * @brief Handle /purses/$PID/create requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_purses_create.h" +#include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_lib.h" +#include "taler-exchange-httpd_keys.h" + + +/** + * Closure for #create_transaction. + */ +struct PurseCreateContext +{ + + /** + * Total actually deposited by all the coins. + */ + struct TALER_Amount deposit_total; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Merge key for the purse. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Encrypted contract of for the purse. + */ + struct TALER_EncryptedContract econtract; + + /** + * Signature of the client affiming this request. + */ + struct TALER_PurseContractSignatureP purse_sig; + + /** + * Fundamental details about the purse. + */ + struct TEH_PurseDetails pd; + + /** + * Array of coins being deposited. + */ + struct TEH_PurseDepositedCoin *coins; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + + /** + * Minimum age for deposits into this purse. + */ + uint32_t min_age; + + /** + * Do we have an @e econtract? + */ + bool no_econtract; + +}; + + +/** + * Execute database transaction for /purses/$PID/create. Runs the transaction + * logic; IF it returns a non-error code, the transaction logic MUST NOT queue + * a MHD response. IF it returns an hard error, the transaction logic MUST + * queue a MHD response and set @a mhd_ret. IF it returns the soft error + * code, the function MAY be called again to retry and MUST not queue a MHD + * response. + * + * @param cls a `struct PurseCreateContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +create_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct PurseCreateContext *pcc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount purse_fee; + bool in_conflict = true; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &purse_fee)); + /* 1) create purse */ + qs = TEH_plugin->insert_purse_request ( + TEH_plugin->cls, + &pcc->pd.purse_pub, + &pcc->merge_pub, + pcc->pd.purse_expiration, + &pcc->pd.h_contract_terms, + pcc->min_age, + TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, + &purse_fee, + &pcc->pd.target_amount, + &pcc->purse_sig, + &in_conflict); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ( + "Failed to store create purse information in database\n"); + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse create"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (in_conflict) + { + struct TALER_PurseMergePublicKeyP merge_pub; + struct GNUNET_TIME_Timestamp purse_expiration; + struct TALER_PrivateContractHashP h_contract_terms; + struct TALER_Amount target_amount; + struct TALER_Amount balance; + struct TALER_PurseContractSignatureP purse_sig; + uint32_t min_age; + + TEH_plugin->rollback (TEH_plugin->cls); + qs = TEH_plugin->get_purse_request (TEH_plugin->cls, + &pcc->pd.purse_pub, + &merge_pub, + &purse_expiration, + &h_contract_terms, + &min_age, + &target_amount, + &balance, + &purse_sig); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + TALER_LOG_WARNING ("Failed to fetch purse information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA), + TALER_JSON_pack_amount ("amount", + &target_amount), + GNUNET_JSON_pack_uint64 ("min_age", + min_age), + GNUNET_JSON_pack_timestamp ("purse_expiration", + purse_expiration), + GNUNET_JSON_pack_data_auto ("purse_sig", + &purse_sig), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &h_contract_terms), + GNUNET_JSON_pack_data_auto ("merge_pub", + &merge_pub)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + /* 2) deposit all coins */ + for (unsigned int i = 0; i<pcc->num_coins; i++) + { + struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; + bool balance_ok = false; + bool conflict = true; + bool too_late = true; + + qs = TEH_make_coin_known (&coin->cpi, + connection, + &coin->known_coin_id, + mhd_ret); + if (qs < 0) + return qs; + qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls, + &pcc->pd.purse_pub, + &coin->cpi.coin_pub, + &coin->amount, + &coin->coin_sig, + &coin->amount_minus_fee, + &balance_ok, + &too_late, + &conflict); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0 != qs); + TALER_LOG_WARNING ( + "Failed to store purse deposit information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse create deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Coin %s has insufficient balance for purse deposit of amount %s\n", + TALER_B2S (&coin->cpi.coin_pub), + TALER_amount2s (&coin->amount)); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &coin->cpi.denom_pub_hash, + &coin->cpi.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (too_late) + { + *mhd_ret + = TALER_MHD_reply_with_ec ( + connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "too late to deposit on purse creation"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (conflict) + { + struct TALER_Amount amount; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_AgeCommitmentHash phac; + char *partner_url = NULL; + + TEH_plugin->rollback (TEH_plugin->cls); + qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls, + &pcc->pd.purse_pub, + &coin->cpi.coin_pub, + &amount, + &h_denom_pub, + &phac, + &coin_sig, + &partner_url); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + TALER_LOG_WARNING ( + "Failed to fetch purse deposit information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get purse deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), + GNUNET_JSON_pack_data_auto ("coin_pub", + &coin_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &coin_sig), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &h_denom_pub), + GNUNET_JSON_pack_data_auto ("h_age_restrictions", + &phac), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("partner_url", + partner_url)), + TALER_JSON_pack_amount ("amount", + &amount)); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + /* 3) if present, persist contract */ + if (! pcc->no_econtract) + { + in_conflict = true; + qs = TEH_plugin->insert_contract (TEH_plugin->cls, + &pcc->pd.purse_pub, + &pcc->econtract, + &in_conflict); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ("Failed to store purse information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse create contract"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (in_conflict) + { + struct TALER_EncryptedContract econtract; + struct GNUNET_HashCode h_econtract; + + qs = TEH_plugin->select_contract_by_purse ( + TEH_plugin->cls, + &pcc->pd.purse_pub, + &econtract); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0 != qs); + TALER_LOG_WARNING ( + "Failed to store fetch contract information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select contract"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_CRYPTO_hash (econtract.econtract, + econtract.econtract_size, + &h_econtract); + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA), + GNUNET_JSON_pack_data_auto ("h_econtract", + &h_econtract), + GNUNET_JSON_pack_data_auto ("econtract_sig", + &econtract.econtract_sig), + GNUNET_JSON_pack_data_auto ("contract_pub", + &econtract.contract_pub)); + GNUNET_free (econtract.econtract); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + return qs; +} + + +/** + * Parse a coin and check signature of the coin and the denomination + * signature over the coin. + * + * @param[in,out] connection our HTTP connection + * @param[in,out] pcc request context + * @param[out] coin coin to initialize + * @param jcoin coin to parse + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +static enum GNUNET_GenericReturnValue +parse_coin (struct MHD_Connection *connection, + struct PurseCreateContext *pcc, + struct TEH_PurseDepositedCoin *coin, + const json_t *jcoin) +{ + enum GNUNET_GenericReturnValue iret; + + if (GNUNET_OK != + (iret = TEH_common_purse_deposit_parse_coin (connection, + coin, + jcoin))) + return iret; + if (GNUNET_OK != + (iret = TEH_common_deposit_check_purse_deposit ( + connection, + coin, + &pcc->pd.purse_pub, + pcc->min_age))) + return iret; + if (0 > + TALER_amount_add (&pcc->deposit_total, + &pcc->deposit_total, + &coin->amount_minus_fee)) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "total deposit contribution")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_purses_create ( + struct MHD_Connection *connection, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root) +{ + struct PurseCreateContext pcc = { + .pd.purse_pub = *purse_pub, + .exchange_timestamp = GNUNET_TIME_timestamp_get () + }; + const json_t *deposits; + json_t *deposit; + unsigned int idx; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("amount", + TEH_currency, + &pcc.pd.target_amount), + GNUNET_JSON_spec_uint32 ("min_age", + &pcc.min_age), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_econtract ("econtract", + &pcc.econtract), + &pcc.no_econtract), + GNUNET_JSON_spec_fixed_auto ("merge_pub", + &pcc.merge_pub), + GNUNET_JSON_spec_fixed_auto ("purse_sig", + &pcc.purse_sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &pcc.pd.h_contract_terms), + GNUNET_JSON_spec_array_const ("deposits", + &deposits), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &pcc.pd.purse_expiration), + GNUNET_JSON_spec_end () + }; + const struct TEH_GlobalFee *gf; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &pcc.deposit_total)); + if (GNUNET_TIME_timestamp_cmp (pcc.pd.purse_expiration, + <, + pcc.exchange_timestamp)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW, + NULL); + } + if (GNUNET_TIME_absolute_is_never (pcc.pd.purse_expiration.abs_time)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER, + NULL); + } + pcc.num_coins = json_array_size (deposits); + if ( (0 == pcc.num_coins) || + (pcc.num_coins > TALER_MAX_FRESH_COINS) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "deposits"); + } + { + struct TEH_KeyStateHandle *keys; + + keys = TEH_keys_get_state (); + if (NULL == keys) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + gf = TEH_keys_global_fee_by_time (keys, + pcc.exchange_timestamp); + } + if (NULL == gf) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create purse: global fees not configured!\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING, + NULL); + } + /* parse deposits */ + pcc.coins = GNUNET_new_array (pcc.num_coins, + struct TEH_PurseDepositedCoin); + json_array_foreach (deposits, idx, deposit) + { + enum GNUNET_GenericReturnValue res; + struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; + + res = parse_coin (connection, + &pcc, + coin, + deposit); + if (GNUNET_OK != res) + { + for (unsigned int i = 0; i<idx; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + } + + if (0 < TALER_amount_cmp (&gf->fees.purse, + &pcc.deposit_total)) + { + GNUNET_break_op (0); + GNUNET_free (pcc.coins); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE, + NULL); + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + + if (GNUNET_OK != + TALER_wallet_purse_create_verify ( + pcc.pd.purse_expiration, + &pcc.pd.h_contract_terms, + &pcc.merge_pub, + pcc.min_age, + &pcc.pd.target_amount, + &pcc.pd.purse_pub, + &pcc.purse_sig)) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID, + NULL); + } + if ( (! pcc.no_econtract) && + (GNUNET_OK != + TALER_wallet_econtract_upload_verify (pcc.econtract.econtract, + pcc.econtract.econtract_size, + &pcc.econtract.contract_pub, + purse_pub, + &pcc.econtract.econtract_sig)) ) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID, + NULL); + } + + + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure"); + } + + /* execute transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "execute purse create", + TEH_MT_REQUEST_PURSE_CREATE, + &mhd_ret, + &create_transaction, + &pcc)) + { + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return mhd_ret; + } + } + + /* generate regular response */ + { + MHD_RESULT res; + + res = TEH_RESPONSE_reply_purse_created (connection, + pcc.exchange_timestamp, + &pcc.deposit_total, + &pcc.pd); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return res; + } +} + + +/* end of taler-exchange-httpd_purses_create.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_create.h b/src/exchange/taler-exchange-httpd_purses_create.h new file mode 100644 index 000000000..7ccba1446 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_create.h @@ -0,0 +1,47 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_create.h + * @brief Handle /purses/$PID/create requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_PURSES_CREATE_H +#define TALER_EXCHANGE_HTTPD_PURSES_CREATE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/purses/$PURSE_PUB/create" request. Parses the JSON, and, if + * successful, passes the JSON data to #create_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses create" being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param purse_pub public key of the purse + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_create ( + struct MHD_Connection *connection, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_purses_delete.c b/src/exchange/taler-exchange-httpd_purses_delete.c new file mode 100644 index 000000000..5bf7c24c9 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_delete.c @@ -0,0 +1,141 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_delete.c + * @brief Handle DELETE /purses/$PID requests; parses the request and + * verifies the signature before handing deletion to the database. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler_dbevents.h" +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_purses_delete.h" +#include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_lib.h" +#include "taler-exchange-httpd_keys.h" + + +MHD_RESULT +TEH_handler_purses_delete ( + struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct MHD_Connection *connection = rc->connection; + struct TALER_PurseContractPublicKeyP purse_pub; + struct TALER_PurseContractSignatureP purse_sig; + bool found; + bool decided; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &purse_pub, + sizeof (purse_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, + args[0]); + } + TALER_MHD_parse_request_header_auto_t (connection, + "Taler-Purse-Signature", + &purse_sig); + if (GNUNET_OK != + TALER_wallet_purse_delete_verify (&purse_pub, + &purse_sig)) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/delete request\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID, + NULL); + } + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure"); + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->do_purse_delete (TEH_plugin->cls, + &purse_pub, + &purse_sig, + &decided, + &found); + if (qs <= 0) + { + TALER_LOG_WARNING ( + "Failed to store delete purse information in database\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse delete"); + } + } + if (! found) + { + return TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + } + if (decided) + { + return TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_PURSE_DELETE_ALREADY_DECIDED, + NULL); + } + { + /* Possible minor optimization: integrate notification with + transaction above... */ + struct TALER_PurseEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), + .purse_pub = purse_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying about purse deletion %s\n", + TALER_B2S (&purse_pub)); + TEH_plugin->event_notify (TEH_plugin->cls, + &rep.header, + NULL, + 0); + } + /* success */ + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_purses_delete.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_delete.h b/src/exchange/taler-exchange-httpd_purses_delete.h new file mode 100644 index 000000000..912dd43a8 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_delete.h @@ -0,0 +1,42 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_delete.h + * @brief Handle DELETE /purses/$PID requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_PURSES_DELETE_H +#define TALER_EXCHANGE_HTTPD_PURSES_DELETE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a DELETE "/purses/$PURSE_PUB" request. + * + * @param rc request details about the request to handle + * @param args argument with the public key of the purse + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_delete ( + struct TEH_RequestContext *rc, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_purses_deposit.c b/src/exchange/taler-exchange-httpd_purses_deposit.c new file mode 100644 index 000000000..8e4d5e41a --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_deposit.c @@ -0,0 +1,507 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_deposit.c + * @brief Handle /purses/$PID/deposit requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler_dbevents.h" +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_purses_deposit.h" +#include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_lib.h" +#include "taler-exchange-httpd_keys.h" + + +/** + * Closure for #deposit_transaction. + */ +struct PurseDepositContext +{ + /** + * Public key of the purse we are creating. + */ + const struct TALER_PurseContractPublicKeyP *purse_pub; + + /** + * Total amount to be put into the purse. + */ + struct TALER_Amount amount; + + /** + * Total actually deposited by all the coins. + */ + struct TALER_Amount deposit_total; + + /** + * When should the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * Hash of the contract (needed for signing). + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Array of coins being deposited. + */ + struct TEH_PurseDepositedCoin *coins; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + + /** + * Minimum age for deposits into this purse. + */ + uint32_t min_age; +}; + + +/** + * Send confirmation of purse creation success to client. + * + * @param connection connection to the client + * @param pcc details about the request that succeeded + * @return MHD result code + */ +static MHD_RESULT +reply_deposit_success (struct MHD_Connection *connection, + const struct PurseDepositContext *pcc) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_purse_created_sign ( + &TEH_keys_exchange_sign_, + pcc->exchange_timestamp, + pcc->purse_expiration, + &pcc->amount, + &pcc->deposit_total, + pcc->purse_pub, + &pcc->h_contract_terms, + &pub, + &sig))) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("total_deposited", + &pcc->deposit_total), + TALER_JSON_pack_amount ("purse_value_after_fees", + &pcc->amount), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + pcc->exchange_timestamp), + GNUNET_JSON_pack_timestamp ("purse_expiration", + pcc->purse_expiration), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &pcc->h_contract_terms), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * Execute database transaction for /purses/$PID/deposit. Runs the transaction + * logic; IF it returns a non-error code, the transaction logic MUST NOT queue + * a MHD response. IF it returns an hard error, the transaction logic MUST + * queue a MHD response and set @a mhd_ret. IF it returns the soft error + * code, the function MAY be called again to retry and MUST not queue a MHD + * response. + * + * @param cls a `struct PurseDepositContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +deposit_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct PurseDepositContext *pcc = cls; + enum GNUNET_DB_QueryStatus qs; + + qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + for (unsigned int i = 0; i<pcc->num_coins; i++) + { + struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; + bool balance_ok = false; + bool conflict = true; + bool too_late = true; + + qs = TEH_make_coin_known (&coin->cpi, + connection, + &coin->known_coin_id, + mhd_ret); + if (qs < 0) + return qs; + qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls, + pcc->purse_pub, + &coin->cpi.coin_pub, + &coin->amount, + &coin->coin_sig, + &coin->amount_minus_fee, + &balance_ok, + &too_late, + &conflict); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0 != qs); + TALER_LOG_WARNING ( + "Failed to store purse deposit information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "do purse deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &coin->cpi.denom_pub_hash, + &coin->cpi.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (too_late) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (conflict) + { + struct TALER_Amount amount; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_AgeCommitmentHash phac; + char *partner_url = NULL; + + TEH_plugin->rollback (TEH_plugin->cls); + qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls, + pcc->purse_pub, + &coin->cpi.coin_pub, + &amount, + &h_denom_pub, + &phac, + &coin_sig, + &partner_url); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + TALER_LOG_WARNING ( + "Failed to fetch purse deposit information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get purse deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), + GNUNET_JSON_pack_data_auto ("coin_pub", + &coin_pub), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &h_denom_pub), + GNUNET_JSON_pack_data_auto ("h_age_commitment", + &phac), + GNUNET_JSON_pack_data_auto ("coin_sig", + &coin_sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("partner_url", + partner_url)), + TALER_JSON_pack_amount ("amount", + &amount)); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + return qs; +} + + +/** + * Parse a coin and check signature of the coin and the denomination + * signature over the coin. + * + * @param[in,out] connection our HTTP connection + * @param[in,out] pcc request context + * @param[out] coin coin to initialize + * @param jcoin coin to parse + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +static enum GNUNET_GenericReturnValue +parse_coin (struct MHD_Connection *connection, + struct PurseDepositContext *pcc, + struct TEH_PurseDepositedCoin *coin, + const json_t *jcoin) +{ + enum GNUNET_GenericReturnValue iret; + + if (GNUNET_OK != + (iret = TEH_common_purse_deposit_parse_coin (connection, + coin, + jcoin))) + return iret; + if (GNUNET_OK != + (iret = TEH_common_deposit_check_purse_deposit ( + connection, + coin, + pcc->purse_pub, + pcc->min_age))) + return iret; + if (0 > + TALER_amount_add (&pcc->deposit_total, + &pcc->deposit_total, + &coin->amount_minus_fee)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "total deposit contribution"); + } + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_purses_deposit ( + struct MHD_Connection *connection, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root) +{ + struct PurseDepositContext pcc = { + .purse_pub = purse_pub, + .exchange_timestamp = GNUNET_TIME_timestamp_get () + }; + const json_t *deposits; + json_t *deposit; + unsigned int idx; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("deposits", + &deposits), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &pcc.deposit_total)); + pcc.num_coins = json_array_size (deposits); + if ( (0 == pcc.num_coins) || + (pcc.num_coins > TALER_MAX_FRESH_COINS) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "deposits"); + } + + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp create_timestamp; + struct GNUNET_TIME_Timestamp merge_timestamp; + bool was_deleted; + bool was_refunded; + + qs = TEH_plugin->select_purse ( + TEH_plugin->cls, + pcc.purse_pub, + &create_timestamp, + &pcc.purse_expiration, + &pcc.amount, + &pcc.deposit_total, + &pcc.h_contract_terms, + &merge_timestamp, + &was_deleted, + &was_refunded); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* handled below */ + } + if (was_refunded || + was_deleted) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_GONE, + was_deleted + ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED + : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, + GNUNET_TIME_timestamp2s (pcc.purse_expiration)); + } + } + + /* parse deposits */ + pcc.coins = GNUNET_new_array (pcc.num_coins, + struct TEH_PurseDepositedCoin); + json_array_foreach (deposits, idx, deposit) + { + enum GNUNET_GenericReturnValue res; + struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; + + res = parse_coin (connection, + &pcc, + coin, + deposit); + if (GNUNET_OK != res) + { + for (unsigned int i = 0; i<idx; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + } + + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure"); + } + + /* execute transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "execute purse deposit", + TEH_MT_REQUEST_PURSE_DEPOSIT, + &mhd_ret, + &deposit_transaction, + &pcc)) + { + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return mhd_ret; + } + } + { + struct TALER_PurseEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), + .purse_pub = *pcc.purse_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying about purse deposit %s\n", + TALER_B2S (pcc.purse_pub)); + TEH_plugin->event_notify (TEH_plugin->cls, + &rep.header, + NULL, + 0); + } + + /* generate regular response */ + { + MHD_RESULT res; + + res = reply_deposit_success (connection, + &pcc); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return res; + } +} + + +/* end of taler-exchange-httpd_purses_deposit.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_deposit.h b/src/exchange/taler-exchange-httpd_purses_deposit.h new file mode 100644 index 000000000..fa587e977 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_deposit.h @@ -0,0 +1,47 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_deposit.h + * @brief Handle /purses/$PID/deposit requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_PURSES_DEPOSIT_H +#define TALER_EXCHANGE_HTTPD_PURSES_DEPOSIT_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/purses/$PURSE_PUB/deposit" request. Parses the JSON, and, if + * successful, passes the JSON data to #deposit_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses deposit" being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param purse_pub public key of the purse + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_deposit ( + struct MHD_Connection *connection, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_purses_get.c b/src/exchange/taler-exchange-httpd_purses_get.c new file mode 100644 index 000000000..22328fe09 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_get.c @@ -0,0 +1,433 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_get.c + * @brief Handle GET /purses/$PID/$TARGET requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler_mhd_lib.h" +#include "taler_dbevents.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_purses_get.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Information about an ongoing /purses GET operation. + */ +struct GetContext +{ + /** + * Kept in a DLL. + */ + struct GetContext *next; + + /** + * Kept in a DLL. + */ + struct GetContext *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Subscription for the database event we are + * waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Subscription for refund event we are + * waiting for. + */ + struct GNUNET_DB_EventHandler *ehr; + + /** + * Public key of our purse. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * When does this purse expire? + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * When was this purse merged? + */ + struct GNUNET_TIME_Timestamp merge_timestamp; + + /** + * How much is the purse (supposed) to be worth? + */ + struct TALER_Amount amount; + + /** + * How much was deposited into the purse so far? + */ + struct TALER_Amount deposited; + + /** + * Hash over the contract of the purse. + */ + struct TALER_PrivateContractHashP h_contract; + + /** + * When will this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * true to wait for merge, false to wait for deposit. + */ + bool wait_for_merge; + + /** + * True if we are still suspended. + */ + bool suspended; +}; + + +/** + * Head of DLL of suspended GET requests. + */ +static struct GetContext *gc_head; + +/** + * Tail of DLL of suspended GET requests. + */ +static struct GetContext *gc_tail; + + +void +TEH_purses_get_cleanup () +{ + struct GetContext *gc; + + while (NULL != (gc = gc_head)) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + if (gc->suspended) + { + gc->suspended = false; + MHD_resume_connection (gc->connection); + } + } +} + + +/** + * Function called once a connection is done to + * clean up the `struct GetContext` state. + * + * @param rc context to clean up for + */ +static void +gc_cleanup (struct TEH_RequestContext *rc) +{ + struct GetContext *gc = rc->rh_ctx; + + GNUNET_assert (! gc->suspended); + if (NULL != gc->eh) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancelling DB event listening\n"); + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + gc->eh); + gc->eh = NULL; + } + if (NULL != gc->ehr) + { + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + gc->ehr); + gc->ehr = NULL; + } + GNUNET_free (gc); +} + + +/** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct TEH_RequestContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct TEH_RequestContext *rc = cls; + struct GetContext *gc = rc->rh_ctx; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Waking up on %p - %p - %s\n", + rc, + gc, + gc->suspended ? "suspended" : "active"); + if (NULL == gc) + return; /* event triggered while main transaction + was still running */ + if (! gc->suspended) + return; /* might get multiple wake-up events */ + gc->suspended = false; + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming from long-polling on purse\n"); + TEH_check_invariants (); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + MHD_resume_connection (gc->connection); + TALER_MHD_daemon_trigger (); + TEH_check_invariants (); + GNUNET_async_scope_restore (&old_scope); +} + + +MHD_RESULT +TEH_handler_purses_get (struct TEH_RequestContext *rc, + const char *const args[2]) +{ + struct GetContext *gc = rc->rh_ctx; + bool purse_deleted; + bool purse_refunded; + MHD_RESULT res; + + if (NULL == gc) + { + gc = GNUNET_new (struct GetContext); + rc->rh_ctx = gc; + rc->rh_cleaner = &gc_cleanup; + gc->connection = rc->connection; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &gc->purse_pub, + sizeof (gc->purse_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, + args[0]); + } + if (0 == strcmp (args[1], + "merge")) + gc->wait_for_merge = true; + else if (0 == strcmp (args[1], + "deposit")) + gc->wait_for_merge = false; + else + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_PURSES_INVALID_WAIT_TARGET, + args[1]); + } + + TALER_MHD_parse_request_timeout (rc->connection, + &gc->timeout); + if ( (GNUNET_TIME_absolute_is_future (gc->timeout)) && + (NULL == gc->eh) ) + { + struct TALER_PurseEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons ( + gc->wait_for_merge + ? TALER_DBEVENT_EXCHANGE_PURSE_MERGED + : TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), + .purse_pub = gc->purse_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting DB event listening on purse %s\n", + TALER_B2S (&gc->purse_pub)); + gc->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (gc->timeout), + &rep.header, + &db_event_cb, + rc); + if (NULL == gc->eh) + { + GNUNET_break (0); + gc->timeout = GNUNET_TIME_UNIT_ZERO_ABS; + } + else + { + struct GNUNET_DB_EventHeaderP repr = { + .size = htons (sizeof (repr)), + .type = htons (TALER_DBEVENT_EXCHANGE_PURSE_REFUNDED), + }; + + gc->ehr = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (gc->timeout), + &repr, + &db_event_cb, + rc); + } + } + } /* end first-time initialization */ + + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp create_timestamp; + + qs = TEH_plugin->select_purse (TEH_plugin->cls, + &gc->purse_pub, + &create_timestamp, + &gc->purse_expiration, + &gc->amount, + &gc->deposited, + &gc->h_contract, + &gc->merge_timestamp, + &purse_deleted, + &purse_refunded); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_purse"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_purse"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* handled below */ + } + } + if (purse_refunded || + purse_deleted) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Purse refunded or deleted\n"); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_GONE, + purse_deleted + ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED + : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, + GNUNET_TIME_timestamp2s ( + gc->purse_expiration)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Deposited amount is %s\n", + TALER_amount2s (&gc->deposited)); + if (GNUNET_TIME_absolute_is_future (gc->timeout) && + ( ((gc->wait_for_merge) && + GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) || + ((! gc->wait_for_merge) && + (0 < + TALER_amount_cmp (&gc->amount, + &gc->deposited))) ) ) + { + gc->suspended = true; + GNUNET_CONTAINER_DLL_insert (gc_head, + gc_tail, + gc); + MHD_suspend_connection (gc->connection); + return MHD_YES; + } + + { + struct GNUNET_TIME_Timestamp dt = GNUNET_TIME_timestamp_get (); + struct TALER_ExchangePublicKeyP exchange_pub; + struct TALER_ExchangeSignatureP exchange_sig; + enum TALER_ErrorCode ec; + + if (GNUNET_TIME_timestamp_cmp (dt, + >, + gc->purse_expiration)) + dt = gc->purse_expiration; + if (0 < + TALER_amount_cmp (&gc->amount, + &gc->deposited)) + { + /* amount > deposited: not yet fully paid */ + dt = GNUNET_TIME_UNIT_ZERO_TS; + } + if (TALER_EC_NONE != + (ec = TALER_exchange_online_purse_status_sign ( + &TEH_keys_exchange_sign_, + gc->merge_timestamp, + dt, + &gc->deposited, + &exchange_pub, + &exchange_sig))) + { + res = TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); + } + else + { + /* Make sure merge_timestamp is omitted if not yet merged */ + if (GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) + gc->merge_timestamp = GNUNET_TIME_UNIT_ZERO_TS; + res = TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("balance", + &gc->deposited), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_pack_timestamp ("purse_expiration", + gc->purse_expiration), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("merge_timestamp", + gc->merge_timestamp)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("deposit_timestamp", + dt)) + ); + } + } + return res; +} + + +/* end of taler-exchange-httpd_purses_get.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_get.h b/src/exchange/taler-exchange-httpd_purses_get.h new file mode 100644 index 000000000..648b01c9a --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_get.h @@ -0,0 +1,51 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_get.h + * @brief Handle /purses/$PURSE_PUB/$TARGET GET requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_PURSES_GET_H +#define TALER_EXCHANGE_HTTPD_PURSES_GET_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown purses-get subsystem. Resumes all + * suspended long-polling clients and cleans up + * data structures. + */ +void +TEH_purses_get_cleanup (void); + + +/** + * Handle a GET "/purses/$PID/$TARGET" request. Parses the + * given "purse_pub" in @a args (which should contain the + * EdDSA public key of a purse) and then respond with the + * status of the purse. + * + * @param rc request context + * @param args array of additional options (length: 2, the purse_pub and a target) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_get (struct TEH_RequestContext *rc, + const char *const args[2]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c new file mode 100644 index 000000000..fb5ce4d90 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_merge.c @@ -0,0 +1,696 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_merge.c + * @brief Handle /purses/$PID/merge requests; parses the POST and JSON and + * verifies the reserve signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_dbevents.h" +#include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_purses_merge.h" +#include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_lib.h" +#include "taler-exchange-httpd_keys.h" + + +/** + * Closure for #merge_transaction. + */ +struct PurseMergeContext +{ + /** + * Public key of the purse we are creating. + */ + const struct TALER_PurseContractPublicKeyP *purse_pub; + + /** + * Total amount to be put into the purse. + */ + struct TALER_Amount target_amount; + + /** + * Current amount in the purse. + */ + struct TALER_Amount balance; + + /** + * When should the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * When the client signed the merge. + */ + struct GNUNET_TIME_Timestamp merge_timestamp; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Merge key for the purse. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Signature of the reservce affiming this request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Signature of the client affiming the merge. + */ + struct TALER_PurseMergeSignatureP merge_sig; + + /** + * Public key of the reserve, as extracted from @e payto_uri. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Hash of the contract terms of the purse. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Fees that apply to this operation. + */ + const struct TALER_WireFeeSet *wf; + + /** + * URI of the account the purse is to be merged into. + * Must be of the form 'payto://taler-reserve/$EXCHANGE_URL/RESERVE_PUB'. + */ + const char *payto_uri; + + /** + * Hash of the @e payto_uri. + */ + struct TALER_PaytoHashP h_payto; + + /** + * KYC status of the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Base URL of the exchange provider hosting the reserve. + */ + char *provider_url; + + /** + * Minimum age for deposits into this purse. + */ + uint32_t min_age; +}; + + +/** + * Send confirmation of purse creation success to client. + * + * @param connection connection to the client + * @param pcc details about the request that succeeded + * @return MHD result code + */ +static MHD_RESULT +reply_merge_success (struct MHD_Connection *connection, + const struct PurseMergeContext *pcc) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + struct TALER_Amount merge_amount; + + if (0 < + TALER_amount_cmp (&pcc->balance, + &pcc->target_amount)) + { + GNUNET_break (0); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_JSON_pack_amount ("balance", + &pcc->balance), + TALER_JSON_pack_amount ("target_amount", + &pcc->target_amount)); + } + if ( (NULL == pcc->provider_url) || + (0 == strcmp (pcc->provider_url, + TEH_base_url)) ) + { + /* wad fee is always zero if we stay at our own exchange */ + merge_amount = pcc->target_amount; + } + else + { +#if WAD_NOT_IMPLEMENTED + /* FIXME: figure out partner, lookup wad fee by partner! #7271 */ + if (0 > + TALER_amount_subtract (&merge_amount, + &pcc->target_amount, + &wad_fee)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &merge_amount)); + } +#else + merge_amount = pcc->target_amount; +#endif + } + if (TALER_EC_NONE != + (ec = TALER_exchange_online_purse_merged_sign ( + &TEH_keys_exchange_sign_, + pcc->exchange_timestamp, + pcc->purse_expiration, + &merge_amount, + pcc->purse_pub, + &pcc->h_contract_terms, + &pcc->reserve_pub, + (NULL != pcc->provider_url) + ? pcc->provider_url + : TEH_base_url, + &pub, + &sig))) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("merge_amount", + &merge_amount), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + pcc->exchange_timestamp), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls a `struct PurseMergeContext` + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +amount_iterator (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct PurseMergeContext *pcc = cls; + enum GNUNET_DB_QueryStatus qs; + + cb (cb_cls, + &pcc->target_amount, + GNUNET_TIME_absolute_get ()); + qs = TEH_plugin->select_merge_amounts_for_kyc_check ( + TEH_plugin->cls, + &pcc->h_payto, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this merge and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); +} + + +/** + * Execute database transaction for /purses/$PID/merge. Runs the transaction + * logic; IF it returns a non-error code, the transaction logic MUST NOT queue + * a MHD response. IF it returns an hard error, the transaction logic MUST + * queue a MHD response and set @a mhd_ret. IF it returns the soft error + * code, the function MAY be called again to retry and MUST not queue a MHD + * response. + * + * @param cls a `struct PurseMergeContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +merge_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct PurseMergeContext *pcc = cls; + enum GNUNET_DB_QueryStatus qs; + bool in_conflict = true; + bool no_balance = true; + bool no_partner = true; + char *required; + + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, + &pcc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &amount_iterator, + pcc, + &required); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "kyc_test_required"); + return qs; + } + if (NULL != required) + { + pcc->kyc.ok = false; + qs = TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + required, + &pcc->h_payto, + &pcc->reserve_pub, + &pcc->kyc.requirement_row); + GNUNET_free (required); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_for_account"); + } + return qs; + } + pcc->kyc.ok = true; + qs = TEH_plugin->do_purse_merge ( + TEH_plugin->cls, + pcc->purse_pub, + &pcc->merge_sig, + pcc->merge_timestamp, + &pcc->reserve_sig, + pcc->provider_url, + &pcc->reserve_pub, + &no_partner, + &no_balance, + &in_conflict); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse merge"); + return qs; + } + if (no_partner) + { + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN, + pcc->provider_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (no_balance) + { + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYMENT_REQUIRED, + TALER_EC_EXCHANGE_PURSE_NOT_FULL, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (in_conflict) + { + struct TALER_PurseMergeSignatureP merge_sig; + struct GNUNET_TIME_Timestamp merge_timestamp; + char *partner_url = NULL; + struct TALER_ReservePublicKeyP reserve_pub; + bool refunded; + + qs = TEH_plugin->select_purse_merge (TEH_plugin->cls, + pcc->purse_pub, + &merge_sig, + &merge_timestamp, + &partner_url, + &reserve_pub, + &refunded); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ( + "Failed to fetch merge purse information from database\n"); + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse merge"); + return qs; + } + if (refunded) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Purse was already refunded\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_GONE, + TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, + NULL); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (0 != + GNUNET_memcmp (&merge_sig, + &pcc->merge_sig)) + { + *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + GNUNET_JSON_pack_timestamp ("merge_timestamp", + merge_timestamp), + GNUNET_JSON_pack_data_auto ("merge_sig", + &merge_sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("partner_url", + partner_url)), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &reserve_pub)); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + /* idempotent! */ + *mhd_ret = reply_merge_success (connection, + pcc); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + return qs; +} + + +MHD_RESULT +TEH_handler_purses_merge ( + struct MHD_Connection *connection, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root) +{ + struct PurseMergeContext pcc = { + .purse_pub = purse_pub, + .exchange_timestamp = GNUNET_TIME_timestamp_get () + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_payto_uri ("payto_uri", + &pcc.payto_uri), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &pcc.reserve_sig), + GNUNET_JSON_spec_fixed_auto ("merge_sig", + &pcc.merge_sig), + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &pcc.merge_timestamp), + GNUNET_JSON_spec_end () + }; + struct TALER_PurseContractSignatureP purse_sig; + enum GNUNET_DB_QueryStatus qs; + bool http; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + + /* Fetch purse details */ + qs = TEH_plugin->get_purse_request (TEH_plugin->cls, + pcc.purse_pub, + &pcc.merge_pub, + &pcc.purse_expiration, + &pcc.h_contract_terms, + &pcc.min_age, + &pcc.target_amount, + &pcc.balance, + &purse_sig); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* continued below */ + break; + } + /* parse 'payto_uri' into pcc.reserve_pub and provider_url */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received payto: `%s'\n", + pcc.payto_uri); + if ( (0 != strncmp (pcc.payto_uri, + "payto://taler-reserve/", + strlen ("payto://taler-reserve/"))) && + (0 != strncmp (pcc.payto_uri, + "payto://taler-reserve-http/", + strlen ("payto://taler-reserve+http/"))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + http = (0 == strncmp (pcc.payto_uri, + "payto://taler-reserve-http/", + strlen ("payto://taler-reserve-http/"))); + + { + const char *host = &pcc.payto_uri[http + ? strlen ("payto://taler-reserve-http/") + : strlen ("payto://taler-reserve/")]; + const char *slash = strchr (host, + '/'); + + if (NULL == slash) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + GNUNET_asprintf (&pcc.provider_url, + "%s://%.*s/", + http ? "http" : "https", + (int) (slash - host), + host); + slash++; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (slash, + strlen (slash), + &pcc.reserve_pub, + sizeof (pcc.reserve_pub))) + { + GNUNET_break_op (0); + GNUNET_free (pcc.provider_url); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + slash++; + } + TALER_payto_hash (pcc.payto_uri, + &pcc.h_payto); + if (0 == strcmp (pcc.provider_url, + TEH_base_url)) + { + /* we use NULL to represent 'self' as the provider */ + GNUNET_free (pcc.provider_url); + } + else + { + char *method = GNUNET_strdup ("FIXME-WAD #7271"); + + /* FIXME-#7271: lookup wire method by pcc.provider_url! */ + pcc.wf = TEH_wire_fees_by_time (pcc.exchange_timestamp, + method); + if (NULL == pcc.wf) + { + MHD_RESULT res; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot merge purse: wire fees not configured!\n"); + res = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_WIRE_FEES_MISSING, + method); + GNUNET_free (method); + return res; + } + GNUNET_free (method); + } + /* check signatures */ + if (GNUNET_OK != + TALER_wallet_purse_merge_verify ( + pcc.payto_uri, + pcc.merge_timestamp, + pcc.purse_pub, + &pcc.merge_pub, + &pcc.merge_sig)) + { + GNUNET_break_op (0); + GNUNET_free (pcc.provider_url); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE, + NULL); + } + { + struct TALER_Amount zero_purse_fee; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (pcc.target_amount.currency, + &zero_purse_fee)); + if (GNUNET_OK != + TALER_wallet_account_merge_verify ( + pcc.merge_timestamp, + pcc.purse_pub, + pcc.purse_expiration, + &pcc.h_contract_terms, + &pcc.target_amount, + &zero_purse_fee, + pcc.min_age, + TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, + &pcc.reserve_pub, + &pcc.reserve_sig)) + { + GNUNET_break_op (0); + GNUNET_free (pcc.provider_url); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE, + NULL); + } + } + + /* execute transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "execute purse merge", + TEH_MT_REQUEST_PURSE_MERGE, + &mhd_ret, + &merge_transaction, + &pcc)) + { + GNUNET_free (pcc.provider_url); + return mhd_ret; + } + } + + + GNUNET_free (pcc.provider_url); + if (! pcc.kyc.ok) + return TEH_RESPONSE_reply_kyc_required (connection, + &pcc.h_payto, + &pcc.kyc); + + { + struct TALER_PurseEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_MERGED), + .purse_pub = *pcc.purse_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying about purse merge\n"); + TEH_plugin->event_notify (TEH_plugin->cls, + &rep.header, + NULL, + 0); + } + + /* generate regular response */ + return reply_merge_success (connection, + &pcc); +} + + +/* end of taler-exchange-httpd_purses_merge.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_merge.h b/src/exchange/taler-exchange-httpd_purses_merge.h new file mode 100644 index 000000000..3bc6e1696 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_merge.h @@ -0,0 +1,46 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_purses_merge.h + * @brief Handle /purses/$PID/merge requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_PURSES_MERGE_H +#define TALER_EXCHANGE_HTTPD_PURSES_MERGE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/purses/$PURSE_PUB/merge" request. Parses the JSON, and, if + * successful, passes the JSON data to #merge_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses merge" being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param purse_pub public key of the purse + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_merge (struct MHD_Connection *connection, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_recoup-refresh.c b/src/exchange/taler-exchange-httpd_recoup-refresh.c index 78a454c85..a5d5b2ab4 100644 --- a/src/exchange/taler-exchange-httpd_recoup-refresh.c +++ b/src/exchange/taler-exchange-httpd_recoup-refresh.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 Affero General Public License as published by the Free Software @@ -55,7 +55,7 @@ struct RecoupContext /** * Key used to blind the coin. */ - const union TALER_DenominationBlindingKeyP *coin_bks; + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; /** * Signature of the coin requesting recoup. @@ -146,6 +146,7 @@ recoup_refresh_transaction (void *cls, *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &pc->coin->denom_pub_hash, &pc->coin->coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -162,7 +163,10 @@ recoup_refresh_transaction (void *cls, * * @param connection the MHD connection to handle * @param coin information about the coin + * @param exchange_vals values contributed by the exchange + * during refresh * @param coin_bks blinding data of the coin (to be checked) + * @param nonce withdraw nonce (if CS is used) * @param coin_sig signature of the coin * @return MHD result code */ @@ -170,13 +174,15 @@ static MHD_RESULT verify_and_execute_recoup_refresh ( struct MHD_Connection *connection, const struct TALER_CoinPublicInfo *coin, - const union TALER_DenominationBlindingKeyP *coin_bks, + const struct TALER_ExchangeWithdrawValues *exchange_vals, + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, + const union GNUNET_CRYPTO_BlindSessionNonce *nonce, const struct TALER_CoinSpendSignatureP *coin_sig) { struct RecoupContext pc; const struct TEH_DenominationKey *dk; MHD_RESULT mret; - struct TALER_BlindedCoinHash h_blind; + struct TALER_BlindedCoinHashP h_blind; /* check denomination exists and is in recoup mode */ dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, @@ -213,6 +219,17 @@ verify_and_execute_recoup_refresh ( } /* check denomination signature */ + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } if (GNUNET_YES != TALER_test_coin_valid (coin, &dk->denom_pub)) @@ -226,6 +243,7 @@ verify_and_execute_recoup_refresh ( } /* check recoup request signature */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_recoup_refresh_verify (&coin->denom_pub_hash, coin_bks, @@ -241,18 +259,18 @@ verify_and_execute_recoup_refresh ( } { - void *coin_ev; - size_t coin_ev_size; - struct TALER_CoinPubHash c_hash; + struct TALER_CoinPubHashP c_hash; + struct TALER_BlindedPlanchet blinded_planchet; if (GNUNET_OK != TALER_denom_blind (&dk->denom_pub, coin_bks, - NULL, /* FIXME-Oec: TALER_AgeHash * */ + nonce, + &coin->h_age_commitment, &coin->coin_pub, + exchange_vals, &c_hash, - &coin_ev, - &coin_ev_size)) + &blinded_planchet)) { GNUNET_break (0); return TALER_MHD_reply_with_error ( @@ -261,10 +279,10 @@ verify_and_execute_recoup_refresh ( TALER_EC_EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED, NULL); } - TALER_coin_ev_hash (coin_ev, - coin_ev_size, + TALER_coin_ev_hash (&blinded_planchet, + &coin->denom_pub_hash, &h_blind); - GNUNET_free (coin_ev); + TALER_blinded_planchet_free (&blinded_planchet); } pc.coin_sig = coin_sig; @@ -322,7 +340,7 @@ verify_and_execute_recoup_refresh ( if (GNUNET_OK != TEH_DB_run_transaction (connection, "run recoup-refresh", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &mhd_ret, &recoup_refresh_transaction, &pc)) @@ -354,18 +372,32 @@ TEH_handler_recoup_refresh (struct MHD_Connection *connection, const json_t *root) { enum GNUNET_GenericReturnValue ret; - struct TALER_CoinPublicInfo coin; - union TALER_DenominationBlindingKeyP coin_bks; + struct TALER_CoinPublicInfo coin = {0}; + union GNUNET_CRYPTO_BlindingSecretP coin_bks; struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_ExchangeWithdrawValues exchange_vals; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + bool no_nonce; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", &coin.denom_pub_hash), TALER_JSON_spec_denom_sig ("denom_sig", &coin.denom_sig), + TALER_JSON_spec_exchange_withdraw_values ("ewv", + &exchange_vals), GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", &coin_bks), GNUNET_JSON_spec_fixed_auto ("coin_sig", &coin_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &coin.h_age_commitment), + &coin.no_age_commitment), + // FIXME: rename to just 'nonce' + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("cs_nonce", + &nonce), + &no_nonce), GNUNET_JSON_spec_end () }; @@ -385,7 +417,11 @@ TEH_handler_recoup_refresh (struct MHD_Connection *connection, res = verify_and_execute_recoup_refresh (connection, &coin, + &exchange_vals, &coin_bks, + no_nonce + ? NULL + : &nonce, &coin_sig); GNUNET_JSON_parse_free (spec); return res; diff --git a/src/exchange/taler-exchange-httpd_recoup-refresh.h b/src/exchange/taler-exchange-httpd_recoup-refresh.h index 25c12fac3..94ae7f889 100644 --- a/src/exchange/taler-exchange-httpd_recoup-refresh.h +++ b/src/exchange/taler-exchange-httpd_recoup-refresh.h @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-exchange-httpd_recoup_refresh.h + * @file taler-exchange-httpd_recoup-refresh.h * @brief Handle /recoup-refresh requests * @author Christian Grothoff */ diff --git a/src/exchange/taler-exchange-httpd_recoup.c b/src/exchange/taler-exchange-httpd_recoup.c index 0deaa8bbb..afbbd7474 100644 --- a/src/exchange/taler-exchange-httpd_recoup.c +++ b/src/exchange/taler-exchange-httpd_recoup.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2017-2021 Taler Systems SA + Copyright (C) 2017-2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -40,9 +40,9 @@ struct RecoupContext { /** - * Hash of the blinded coin. + * Hash identifying the withdraw request. */ - struct TALER_BlindedCoinHash h_blind; + struct TALER_BlindedCoinHashP h_coin_ev; /** * Set by #recoup_transaction() to the reserve that will @@ -58,7 +58,7 @@ struct RecoupContext /** * Key used to blind the coin. */ - const union TALER_DenominationBlindingKeyP *coin_bks; + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; /** * Signature of the coin requesting recoup. @@ -149,6 +149,7 @@ recoup_transaction (void *cls, *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &pc->coin->denom_pub_hash, &pc->coin->coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -165,7 +166,10 @@ recoup_transaction (void *cls, * * @param connection the MHD connection to handle * @param coin information about the coin + * @param exchange_vals values contributed by the exchange + * during withdrawal * @param coin_bks blinding data of the coin (to be checked) + * @param nonce coin's nonce if CS is used * @param coin_sig signature of the coin * @return MHD result code */ @@ -173,7 +177,9 @@ static MHD_RESULT verify_and_execute_recoup ( struct MHD_Connection *connection, const struct TALER_CoinPublicInfo *coin, - const union TALER_DenominationBlindingKeyP *coin_bks, + const struct TALER_ExchangeWithdrawValues *exchange_vals, + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, + const union GNUNET_CRYPTO_BlindSessionNonce *nonce, const struct TALER_CoinSpendSignatureP *coin_sig) { struct RecoupContext pc; @@ -215,6 +221,17 @@ verify_and_execute_recoup ( } /* check denomination signature */ + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } if (GNUNET_YES != TALER_test_coin_valid (coin, &dk->denom_pub)) @@ -228,6 +245,7 @@ verify_and_execute_recoup ( } /* check recoup request signature */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_recoup_verify (&coin->denom_pub_hash, coin_bks, @@ -242,19 +260,22 @@ verify_and_execute_recoup ( NULL); } + /* re-compute client-side blinding so we can + (a bit later) check that this coin was indeed + signed by us. */ { - void *coin_ev; - size_t coin_ev_size; - struct TALER_CoinPubHash c_hash; + struct TALER_CoinPubHashP c_hash; + struct TALER_BlindedPlanchet blinded_planchet; if (GNUNET_OK != TALER_denom_blind (&dk->denom_pub, coin_bks, - NULL, /* FIXME-Oec: TALER_AgeHash * */ + nonce, + &coin->h_age_commitment, &coin->coin_pub, + exchange_vals, &c_hash, - &coin_ev, - &coin_ev_size)) + &blinded_planchet)) { GNUNET_break (0); return TALER_MHD_reply_with_error ( @@ -263,10 +284,10 @@ verify_and_execute_recoup ( TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, NULL); } - TALER_coin_ev_hash (coin_ev, - coin_ev_size, - &pc.h_blind); - GNUNET_free (coin_ev); + TALER_coin_ev_hash (&blinded_planchet, + &coin->denom_pub_hash, + &pc.h_coin_ev); + TALER_blinded_planchet_free (&blinded_planchet); } pc.coin_sig = coin_sig; @@ -292,7 +313,7 @@ verify_and_execute_recoup ( enum GNUNET_DB_QueryStatus qs; qs = TEH_plugin->get_reserve_by_h_blind (TEH_plugin->cls, - &pc.h_blind, + &pc.h_coin_ev, &pc.reserve_pub, &pc.reserve_out_serial_id); if (0 > qs) @@ -308,7 +329,7 @@ verify_and_execute_recoup ( { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Recoup requested for unknown envelope %s\n", - GNUNET_h2s (&pc.h_blind.hash)); + GNUNET_h2s (&pc.h_coin_ev.hash)); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_NOT_FOUND, @@ -324,7 +345,7 @@ verify_and_execute_recoup ( if (GNUNET_OK != TEH_DB_run_transaction (connection, "run recoup", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &mhd_ret, &recoup_transaction, &pc)) @@ -357,17 +378,31 @@ TEH_handler_recoup (struct MHD_Connection *connection, { enum GNUNET_GenericReturnValue ret; struct TALER_CoinPublicInfo coin; - union TALER_DenominationBlindingKeyP coin_bks; + union GNUNET_CRYPTO_BlindingSecretP coin_bks; struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_ExchangeWithdrawValues exchange_vals; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + bool no_nonce; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", &coin.denom_pub_hash), TALER_JSON_spec_denom_sig ("denom_sig", &coin.denom_sig), + TALER_JSON_spec_exchange_withdraw_values ("ewv", + &exchange_vals), GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", &coin_bks), GNUNET_JSON_spec_fixed_auto ("coin_sig", &coin_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &coin.h_age_commitment), + &coin.no_age_commitment), + // FIXME: should be renamed to just 'nonce'! + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("cs_nonce", + &nonce), + &no_nonce), GNUNET_JSON_spec_end () }; @@ -387,7 +422,11 @@ TEH_handler_recoup (struct MHD_Connection *connection, res = verify_and_execute_recoup (connection, &coin, + &exchange_vals, &coin_bks, + no_nonce + ? NULL + : &nonce, &coin_sig); GNUNET_JSON_parse_free (spec); return res; diff --git a/src/exchange/taler-exchange-httpd_refreshes_reveal.c b/src/exchange/taler-exchange-httpd_refreshes_reveal.c index 30a7294c1..5630051cf 100644 --- a/src/exchange/taler-exchange-httpd_refreshes_reveal.c +++ b/src/exchange/taler-exchange-httpd_refreshes_reveal.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2019, 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 Affero General Public License as published by the Free Software @@ -32,17 +32,11 @@ /** - * Maximum number of fresh coins we allow per refresh operation. - */ -#define MAX_FRESH_COINS 256 - - -/** * Send a response for "/refreshes/$RCH/reveal". * * @param connection the connection to send the response to * @param num_freshcoins number of new coins for which we reveal data - * @param sigs array of @a num_freshcoins signatures revealed + * @param rrcs array of @a num_freshcoins signatures revealed * @return a MHD result code */ static MHD_RESULT @@ -109,15 +103,35 @@ struct RevealContext const struct TEH_DenominationKey **dks; /** + * Age commitment that was used for the original coin. If not NULL, its hash + * should be the same as melt.session.h_age_commitment. + */ + struct TALER_AgeCommitment *old_age_commitment; + + /** + * Array of information about fresh coins being revealed. + */ + struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrcs; + + /** * Envelopes to be signed. */ struct TALER_RefreshCoinData *rcds; /** + * Refresh master secret. + */ + struct TALER_RefreshMasterSecretP rms; + + /** * Size of the @e dks, @e rcds and @e ev_sigs arrays (if non-NULL). */ unsigned int num_fresh_coins; + /** + * True if @e rms was not provided. + */ + bool no_rms; }; @@ -142,6 +156,89 @@ check_commitment (struct RevealContext *rctx, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { + const union GNUNET_CRYPTO_BlindSessionNonce *nonces[rctx->num_fresh_coins]; + + memset (nonces, + 0, + sizeof (nonces)); + for (unsigned int j = 0; j<rctx->num_fresh_coins; j++) + { + const struct TALER_DenominationPublicKey *dk = &rctx->dks[j]->denom_pub; + + if (dk->bsign_pub_key->cipher != + rctx->rcds[j].blinded_planchet.blinded_message->cipher) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL); + return GNUNET_SYSERR; + } + switch (dk->bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + return GNUNET_SYSERR; + case GNUNET_CRYPTO_BSA_RSA: + continue; + case GNUNET_CRYPTO_BSA_CS: + nonces[j] + = (const union GNUNET_CRYPTO_BlindSessionNonce *) + &rctx->rcds[j].blinded_planchet.blinded_message->details. + cs_blinded_message.nonce; + break; + } + } + + // OPTIMIZE: do this in batch later! + for (unsigned int j = 0; j<rctx->num_fresh_coins; j++) + { + const struct TALER_DenominationPublicKey *dk = &rctx->dks[j]->denom_pub; + struct TALER_ExchangeWithdrawValues *alg_values + = &rctx->rrcs[j].exchange_vals; + struct GNUNET_CRYPTO_BlindingInputValues *bi; + + bi = GNUNET_new (struct GNUNET_CRYPTO_BlindingInputValues); + alg_values->blinding_inputs = bi; + bi->rc = 1; + bi->cipher = dk->bsign_pub_key->cipher; + switch (dk->bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_assert (0); + return GNUNET_SYSERR; + case GNUNET_CRYPTO_BSA_RSA: + continue; + case GNUNET_CRYPTO_BSA_CS: + { + enum TALER_ErrorCode ec; + const struct TEH_CsDeriveData cdd = { + .h_denom_pub = &rctx->rrcs[j].h_denom_pub, + .nonce = &nonces[j]->cs_nonce + }; + + ec = TEH_keys_denomination_cs_r_pub ( + &cdd, + true, + &bi->details.cs_values); + if (TALER_EC_NONE != ec) + { + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + ec, + NULL); + return GNUNET_SYSERR; + } + } + } + } /* Verify commitment */ { /* Note that the contents of rcs[melt.session.noreveal_index] @@ -169,9 +266,12 @@ check_commitment (struct RevealContext *rctx, const struct TALER_TransferPrivateKeyP *tpriv = &rctx->transfer_privs[i - off]; struct TALER_TransferSecretP ts; + struct TALER_AgeCommitmentHash h = {0}; + struct TALER_AgeCommitmentHash *hac = NULL; GNUNET_CRYPTO_ecdhe_key_get_public (&tpriv->ecdhe_priv, &rce->transfer_pub.ecdhe_pub); + TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_ECDH]++; TALER_link_reveal_transfer_secret (tpriv, &rctx->melt.session.coin.coin_pub, &ts); @@ -180,26 +280,64 @@ check_commitment (struct RevealContext *rctx, for (unsigned int j = 0; j<rctx->num_fresh_coins; j++) { struct TALER_RefreshCoinData *rcd = &rce->new_coins[j]; - struct TALER_PlanchetSecretsP ps; - struct TALER_PlanchetDetail pd; - struct TALER_CoinPubHash c_hash; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; + const struct TALER_ExchangeWithdrawValues *alg_value + = &rctx->rrcs[j].exchange_vals; + struct TALER_PlanchetDetail pd = {0}; + struct TALER_CoinPubHashP c_hash; + struct TALER_PlanchetMasterSecretP ps; rcd->dk = &rctx->dks[j]->denom_pub; - TALER_planchet_setup_refresh (&ts, - j, - &ps); + TALER_transfer_secret_to_planchet_secret (&ts, + j, + &ps); + TALER_planchet_setup_coin_priv (&ps, + alg_value, + &coin_priv); + TALER_planchet_blinding_secret_create (&ps, + alg_value, + &bks); + /* Calculate, if applicable, the age commitment and its hash, from + * the transfer_secret and the old age commitment. */ + if (NULL != rctx->old_age_commitment) + { + struct TALER_AgeCommitmentProof acp = { + /* we only need the commitment, not the proof, for the call to + * TALER_age_commitment_derive. */ + .commitment = *(rctx->old_age_commitment) + }; + struct TALER_AgeCommitmentProof nacp = {0}; + + GNUNET_assert (GNUNET_OK == + TALER_age_commitment_derive ( + &acp, + &ts.key, + &nacp)); + TALER_age_commitment_hash (&nacp.commitment, + &h); + TALER_age_commitment_proof_free (&nacp); + hac = &h; + } + GNUNET_assert (GNUNET_OK == TALER_planchet_prepare (rcd->dk, - &ps, + alg_value, + &bks, + nonces[j], + &coin_priv, + hac, &c_hash, &pd)); - rcd->coin_ev = pd.coin_ev; - rcd->coin_ev_size = pd.coin_ev_size; + rcd->blinded_planchet = pd.blinded_planchet; } } } TALER_refresh_get_commitment (&rc_expected, TALER_CNC_KAPPA, + rctx->no_rms + ? NULL + : &rctx->rms, rctx->num_fresh_coins, rcs, &rctx->melt.session.coin.coin_pub, @@ -216,7 +354,7 @@ check_commitment (struct RevealContext *rctx, { struct TALER_RefreshCoinData *rcd = &rce->new_coins[j]; - GNUNET_free (rcd->coin_ev); + TALER_blinded_planchet_free (&rcd->blinded_planchet); } GNUNET_free (rce->new_coins); } @@ -248,7 +386,7 @@ check_commitment (struct RevealContext *rctx, if ( (0 > TALER_amount_add (&total, - &rctx->dks[i]->meta.fee_withdraw, + &rctx->dks[i]->meta.fees.withdraw, &rctx->dks[i]->meta.value)) || (0 > TALER_amount_add (&refresh_cost, @@ -285,24 +423,29 @@ check_commitment (struct RevealContext *rctx, * @param rctx context for the operation, partially built at this time * @param link_sigs_json link signatures in JSON format * @param new_denoms_h_json requests for fresh coins to be created + * @param old_age_commitment_json age commitment that went into the withdrawal, maybe NULL * @param coin_evs envelopes of gamma-selected coins to be signed * @return MHD result code */ static MHD_RESULT -resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, - struct RevealContext *rctx, - const json_t *link_sigs_json, - const json_t *new_denoms_h_json, - const json_t *coin_evs) +resolve_refreshes_reveal_denominations ( + struct MHD_Connection *connection, + struct RevealContext *rctx, + const json_t *link_sigs_json, + const json_t *new_denoms_h_json, + const json_t *old_age_commitment_json, + const json_t *coin_evs) { unsigned int num_fresh_coins = json_array_size (new_denoms_h_json); - /* We know num_fresh_coins is bounded by #MAX_FRESH_COINS, so this is safe */ + /* We know num_fresh_coins is bounded by #TALER_MAX_FRESH_COINS, so this is safe */ const struct TEH_DenominationKey *dks[num_fresh_coins]; + const struct TEH_DenominationKey *old_dk; struct TALER_RefreshCoinData rcds[num_fresh_coins]; struct TALER_EXCHANGEDB_RefreshRevealedCoin rrcs[num_fresh_coins]; MHD_RESULT ret; struct TEH_KeyStateHandle *ksh; uint64_t melt_serial_id; + enum GNUNET_DB_QueryStatus qs; memset (dks, 0, sizeof (dks)); memset (rrcs, 0, sizeof (rrcs)); @@ -317,6 +460,61 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); } + + /* lookup old_coin_pub in database */ + { + enum GNUNET_DB_QueryStatus qs; + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + (qs = TEH_plugin->get_melt (TEH_plugin->cls, + &rctx->rc, + &rctx->melt, + &melt_serial_id))) + { + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN, + NULL); + break; + case GNUNET_DB_STATUS_HARD_ERROR: + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "melt"); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + default: + GNUNET_break (0); /* should be impossible */ + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + break; + } + goto cleanup; + } + if (rctx->melt.session.noreveal_index >= TALER_CNC_KAPPA) + { + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "melt"); + goto cleanup; + } + } + + old_dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + &rctx->melt.session.coin.denom_pub_hash, + connection, + &ret); + if (NULL == old_dk) + return ret; + /* Parse denomination key hashes */ for (unsigned int i = 0; i<num_fresh_coins; i++) { @@ -334,13 +532,22 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, -1); if (GNUNET_OK != res) return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - dks[i] = TEH_keys_denomination_by_hash2 (ksh, - &rrcs[i].h_denom_pub, - connection, - &ret); + dks[i] = TEH_keys_denomination_by_hash_from_state (ksh, + &rrcs[i].h_denom_pub, + connection, + &ret); if (NULL == dks[i]) return ret; - + if ( (GNUNET_CRYPTO_BSA_CS == + dks[i]->denom_pub.bsign_pub_key->cipher) && + (rctx->no_rms) ) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "rms"); + } if (GNUNET_TIME_absolute_is_past (dks[i]->meta.expire_withdraw.abs_time)) { /* This denomination is past the expiration time for withdraws */ @@ -375,9 +582,8 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, { struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i]; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_varsize (NULL, - &rrc->coin_ev, - &rrc->coin_ev_size), + TALER_JSON_spec_blinded_planchet (NULL, + &rrc->blinded_planchet), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; @@ -390,59 +596,85 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, if (GNUNET_OK != res) { for (unsigned int j = 0; j<i; j++) - GNUNET_free (rrcs[j].coin_ev); + TALER_blinded_planchet_free (&rrcs[j].blinded_planchet); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } - GNUNET_CRYPTO_hash (rrc->coin_ev, - rrc->coin_ev_size, - &rrc->coin_envelope_hash.hash); + TALER_coin_ev_hash (&rrc->blinded_planchet, + &rrcs[i].h_denom_pub, + &rrc->coin_envelope_hash); } - /* lookup old_coin_pub in database */ + if (TEH_age_restriction_enabled && + ((NULL == old_age_commitment_json) != + TALER_AgeCommitmentHash_isNullOrZero ( + &rctx->melt.session.coin.h_age_commitment))) { - enum GNUNET_DB_QueryStatus qs; + GNUNET_break (0); + return MHD_NO; + } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - (qs = TEH_plugin->get_melt (TEH_plugin->cls, - &rctx->rc, - &rctx->melt, - &melt_serial_id))) + /* Reconstruct the old age commitment and verify its hash matches the one + * from the melt request */ + if (TEH_age_restriction_enabled && + (NULL != old_age_commitment_json)) + { + enum GNUNET_GenericReturnValue res; + struct TALER_AgeCommitment *oac; + size_t ng = json_array_size (old_age_commitment_json); + bool failed = true; + + /* Has been checked in handle_refreshes_reveal_json() */ + GNUNET_assert (ng == TEH_age_restriction_config.num_groups); + + rctx->old_age_commitment = GNUNET_new (struct TALER_AgeCommitment); + oac = rctx->old_age_commitment; + oac->mask = old_dk->meta.age_mask; + oac->num = ng; + oac->keys = GNUNET_new_array (ng, struct TALER_AgeCommitmentPublicKeyP); + + /* Extract old age commitment */ + for (unsigned int i = 0; i< ng; i++) { - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN, - NULL); - break; - case GNUNET_DB_STATUS_HARD_ERROR: - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "melt"); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - default: - GNUNET_break (0); /* should be impossible */ - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - break; - } - goto cleanup; + struct GNUNET_JSON_Specification ac_spec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &oac->keys[i]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_array (connection, + old_age_commitment_json, + ac_spec, + i, + -1); + + GNUNET_break_op (GNUNET_OK == res); + if (GNUNET_OK != res) + goto clean_age; } - if (rctx->melt.session.noreveal_index >= TALER_CNC_KAPPA) + + /* Sanity check: Compare hash from melting with hash of this age commitment */ { - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "melt"); - goto cleanup; + struct TALER_AgeCommitmentHash hac = {0}; + TALER_age_commitment_hash (oac, &hac); + if (0 != memcmp (&hac, + &rctx->melt.session.coin.h_age_commitment, + sizeof(struct TALER_AgeCommitmentHash))) + goto clean_age; + } + + failed = false; + +clean_age: + if (failed) + { + TALER_age_commitment_free (oac); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, + "old_age_commitment"); } } + /* Parse link signatures array */ for (unsigned int i = 0; i<num_fresh_coins; i++) { @@ -460,7 +692,9 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, -1); if (GNUNET_OK != res) return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + /* Check signature */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_link_verify ( &rrcs[i].h_denom_pub, @@ -485,12 +719,24 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, const struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i]; struct TALER_RefreshCoinData *rcd = &rcds[i]; - rcd->coin_ev = rrc->coin_ev; - rcd->coin_ev_size = rrc->coin_ev_size; + rcd->blinded_planchet = rrc->blinded_planchet; rcd->dk = &dks[i]->denom_pub; + if (rcd->blinded_planchet.blinded_message->cipher != + rcd->dk->bsign_pub_key->cipher) + { + GNUNET_break_op (0); + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH)); + goto cleanup; + } } + rctx->dks = dks; rctx->rcds = rcds; + rctx->rrcs = rrcs; if (GNUNET_OK != check_commitment (rctx, connection, @@ -500,17 +746,23 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Creating %u signatures\n", (unsigned int) rctx->num_fresh_coins); + /* create fresh coin signatures */ - for (unsigned int i = 0; i<rctx->num_fresh_coins; i++) { - enum TALER_ErrorCode ec = TALER_EC_NONE; + struct TEH_CoinSignData csds[rctx->num_fresh_coins]; + struct TALER_BlindedDenominationSignature bss[rctx->num_fresh_coins]; + enum TALER_ErrorCode ec; - rrcs[i].coin_sig - = TEH_keys_denomination_sign ( - &rrcs[i].h_denom_pub, - rcds[i].coin_ev, - rcds[i].coin_ev_size, - &ec); + for (unsigned int i = 0; i<rctx->num_fresh_coins; i++) + { + csds[i].h_denom_pub = &rrcs[i].h_denom_pub; + csds[i].bp = &rcds[i].blinded_planchet; + } + ec = TEH_keys_denomination_batch_sign ( + rctx->num_fresh_coins, + csds, + true, + bss); if (TALER_EC_NONE != ec) { GNUNET_break (0); @@ -519,39 +771,88 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, NULL); goto cleanup; } + + for (unsigned int i = 0; i<rctx->num_fresh_coins; i++) + { + rrcs[i].coin_sig = bss[i]; + rrcs[i].blinded_planchet = rcds[i].blinded_planchet; + } } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Signatures ready, starting DB interaction\n"); - /* Persist operation result in DB */ + + + for (unsigned int r = 0; r<MAX_TRANSACTION_COMMIT_RETRIES; r++) { - enum GNUNET_DB_QueryStatus qs; + bool changed; - for (unsigned int i = 0; i<rctx->num_fresh_coins; i++) + /* Persist operation result in DB */ + if (GNUNET_OK != + TEH_plugin->start (TEH_plugin->cls, + "insert_refresh_reveal batch")) { - struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i]; + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + goto cleanup; + } - rrc->coin_ev = rcds[i].coin_ev; - rrc->coin_ev_size = rcds[i].coin_ev_size; + qs = TEH_plugin->insert_refresh_reveal ( + TEH_plugin->cls, + melt_serial_id, + num_fresh_coins, + rrcs, + TALER_CNC_KAPPA - 1, + rctx->transfer_privs, + &rctx->gamma_tp); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + TEH_plugin->rollback (TEH_plugin->cls); + continue; } - qs = TEH_plugin->insert_refresh_reveal (TEH_plugin->cls, - melt_serial_id, - num_fresh_coins, - rrcs, - TALER_CNC_KAPPA - 1, - rctx->transfer_privs, - &rctx->gamma_tp); /* 0 == qs is ok, as we did not check for repeated requests */ - if (0 > qs) + if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); + TEH_plugin->rollback (TEH_plugin->cls); ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "insert_refresh_reveal"); goto cleanup; } + changed = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + qs = TEH_plugin->commit (TEH_plugin->cls); + if (qs >= 0) + { + if (changed) + TEH_METRICS_num_success[TEH_MT_SUCCESS_REFRESH_REVEAL]++; + break; /* success */ + } + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + TEH_plugin->rollback (TEH_plugin->cls); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + goto cleanup; + } + TEH_plugin->rollback (TEH_plugin->cls); + } + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + GNUNET_break (0); + TEH_plugin->rollback (TEH_plugin->cls); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + goto cleanup; } - /* Generate final (positive) response */ ret = reply_refreshes_reveal_success (connection, num_fresh_coins, @@ -562,9 +863,12 @@ cleanup: for (unsigned int i = 0; i<num_fresh_coins; i++) { struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i]; + struct TALER_ExchangeWithdrawValues *alg_values + = &rrcs[i].exchange_vals; + GNUNET_free (alg_values->blinding_inputs); TALER_blinded_denom_sig_free (&rrc->coin_sig); - GNUNET_free (rrc->coin_ev); + TALER_blinded_planchet_free (&rrc->blinded_planchet); } return ret; } @@ -577,11 +881,18 @@ cleanup: * revealed information is valid then returns the signed refreshed * coins. * + * If the denomination has age restriction support, the array of EDDSA public + * keys, one for each age group that was activated during the withdrawal + * by the parent/ward, must be provided in old_age_commitment. The hash of + * this array must be the same as the h_age_commitment of the persisted reveal + * request. + * * @param connection the MHD connection to handle * @param rctx context for the operation, partially built at this time * @param tp_json private transfer keys in JSON format * @param link_sigs_json link signatures in JSON format * @param new_denoms_h_json requests for fresh coins to be created + * @param old_age_commitment_json array of EDDSA public keys in JSON, used for age restriction, maybe NULL * @param coin_evs envelopes of gamma-selected coins to be signed * @return MHD result code */ @@ -591,19 +902,20 @@ handle_refreshes_reveal_json (struct MHD_Connection *connection, const json_t *tp_json, const json_t *link_sigs_json, const json_t *new_denoms_h_json, + const json_t *old_age_commitment_json, const json_t *coin_evs) { unsigned int num_fresh_coins = json_array_size (new_denoms_h_json); unsigned int num_tprivs = json_array_size (tp_json); GNUNET_assert (num_tprivs == TALER_CNC_KAPPA - 1); /* checked just earlier */ - if ( (num_fresh_coins >= MAX_FRESH_COINS) || + if ( (num_fresh_coins >= TALER_MAX_FRESH_COINS) || (0 == num_fresh_coins) ) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE, + TALER_EC_EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE, NULL); } @@ -626,6 +938,19 @@ handle_refreshes_reveal_json (struct MHD_Connection *connection, "new_denoms/link_sigs"); } + /* Sanity check of age commitment: If it was provided, it _must_ be an array + * of the size the # of age groups */ + if (NULL != old_age_commitment_json + && TEH_age_restriction_config.num_groups != + json_array_size (old_age_commitment_json)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, + "old_age_commitment"); + } + /* Parse transfer private keys array */ for (unsigned int i = 0; i<num_tprivs; i++) { @@ -649,6 +974,7 @@ handle_refreshes_reveal_json (struct MHD_Connection *connection, rctx, link_sigs_json, new_denoms_h_json, + old_age_commitment_json, coin_evs); } @@ -658,22 +984,31 @@ TEH_handler_reveal (struct TEH_RequestContext *rc, const json_t *root, const char *const args[2]) { - json_t *coin_evs; - json_t *transfer_privs; - json_t *link_sigs; - json_t *new_denoms_h; + const json_t *coin_evs; + const json_t *transfer_privs; + const json_t *link_sigs; + const json_t *new_denoms_h; + const json_t *old_age_commitment; struct RevealContext rctx; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("transfer_pub", &rctx.gamma_tp), - GNUNET_JSON_spec_json ("transfer_privs", - &transfer_privs), - GNUNET_JSON_spec_json ("link_sigs", - &link_sigs), - GNUNET_JSON_spec_json ("coin_evs", - &coin_evs), - GNUNET_JSON_spec_json ("new_denoms_h", - &new_denoms_h), + GNUNET_JSON_spec_array_const ("transfer_privs", + &transfer_privs), + GNUNET_JSON_spec_array_const ("link_sigs", + &link_sigs), + GNUNET_JSON_spec_array_const ("coin_evs", + &coin_evs), + GNUNET_JSON_spec_array_const ("new_denoms_h", + &new_denoms_h), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("old_age_commitment", + &old_age_commitment), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("rms", + &rctx.rms), + &rctx.no_rms), GNUNET_JSON_spec_end () }; @@ -719,7 +1054,6 @@ TEH_handler_reveal (struct TEH_RequestContext *rc, /* Note we do +1 as 1 row (cut-and-choose!) is missing! */ if (TALER_CNC_KAPPA != json_array_size (transfer_privs) + 1) { - GNUNET_JSON_parse_free (spec); GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, @@ -727,18 +1061,13 @@ TEH_handler_reveal (struct TEH_RequestContext *rc, NULL); } - { - MHD_RESULT res; - - res = handle_refreshes_reveal_json (rc->connection, - &rctx, - transfer_privs, - link_sigs, - new_denoms_h, - coin_evs); - GNUNET_JSON_parse_free (spec); - return res; - } + return handle_refreshes_reveal_json (rc->connection, + &rctx, + transfer_privs, + link_sigs, + new_denoms_h, + old_age_commitment, + coin_evs); } diff --git a/src/exchange/taler-exchange-httpd_refund.c b/src/exchange/taler-exchange-httpd_refund.c index 9cc019a14..b8bcf7c60 100644 --- a/src/exchange/taler-exchange-httpd_refund.c +++ b/src/exchange/taler-exchange-httpd_refund.c @@ -50,22 +50,18 @@ reply_refund_success (struct MHD_Connection *connection, { struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; - struct TALER_RefundConfirmationPS rc = { - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND), - .purpose.size = htonl (sizeof (rc)), - .h_contract_terms = refund->h_contract_terms, - .coin_pub = *coin_pub, - .merchant = refund->merchant_pub, - .rtransaction_id = GNUNET_htonll (refund->rtransaction_id) - }; enum TALER_ErrorCode ec; - TALER_amount_hton (&rc.refund_amount, - &refund->refund_amount); if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign (&rc, - &pub, - &sig))) + (ec = TALER_exchange_online_refund_confirmation_sign ( + &TEH_keys_exchange_sign_, + &refund->h_contract_terms, + coin_pub, + &refund->merchant_pub, + refund->rtransaction_id, + &refund->refund_amount, + &pub, + &sig))) { return TALER_MHD_reply_with_ec (connection, ec, @@ -162,10 +158,11 @@ refund_transaction (void *cls, } if (conflict) { - TEH_plugin->rollback (TEH_plugin->cls); + GNUNET_break_op (0); *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, + &refund->coin.denom_pub_hash, &refund->coin.coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -179,10 +176,10 @@ refund_transaction (void *cls, } if (! refund_ok) { - TEH_plugin->rollback (TEH_plugin->cls); *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT, + &refund->coin.denom_pub_hash, &refund->coin.coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -204,11 +201,11 @@ static MHD_RESULT verify_and_execute_refund (struct MHD_Connection *connection, struct TALER_EXCHANGEDB_Refund *refund) { - struct TALER_DenominationHash denom_hash; struct RefundContext rctx = { .refund = refund }; + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_merchant_refund_verify (&refund->coin.coin_pub, &refund->details.h_contract_terms, @@ -231,15 +228,16 @@ verify_and_execute_refund (struct MHD_Connection *connection, qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls, &refund->coin.coin_pub, &rctx.known_coin_id, - &denom_hash); + &refund->coin.denom_pub_hash); if (0 > qs) { MHD_RESULT res; char *dhs; GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - dhs = GNUNET_STRINGS_data_to_string_alloc (&denom_hash, - sizeof (denom_hash)); + dhs = GNUNET_STRINGS_data_to_string_alloc ( + &refund->coin.denom_pub_hash, + sizeof (refund->coin.denom_pub_hash)); res = TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_REFUND_COIN_NOT_FOUND, @@ -254,7 +252,7 @@ verify_and_execute_refund (struct MHD_Connection *connection, struct TEH_DenominationKey *dk; MHD_RESULT mret; - dk = TEH_keys_denomination_by_hash (&denom_hash, + dk = TEH_keys_denomination_by_hash (&refund->coin.denom_pub_hash, connection, &mret); if (NULL == dk) @@ -264,8 +262,8 @@ verify_and_execute_refund (struct MHD_Connection *connection, GNUNET_break (0); return mret; } - refund->details.refund_fee = dk->meta.fee_refund; - rctx.deposit_fee = dk->meta.fee_deposit; + refund->details.refund_fee = dk->meta.fees.refund; + rctx.deposit_fee = dk->meta.fees.deposit; } /* Finally run the actual transaction logic */ @@ -275,7 +273,7 @@ verify_and_execute_refund (struct MHD_Connection *connection, if (GNUNET_OK != TEH_DB_run_transaction (connection, "run refund", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &mhd_ret, &refund_transaction, &rctx)) diff --git a/src/exchange/taler-exchange-httpd_reserves_attest.c b/src/exchange/taler-exchange-httpd_reserves_attest.c new file mode 100644 index 000000000..7bbebaad7 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_attest.c @@ -0,0 +1,385 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_attest.c + * @brief Handle /reserves/$RESERVE_PUB/attest requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_dbevents.h" +#include "taler_kyclogic_lib.h" +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_reserves_attest.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How far do we allow a client's time to be off when + * checking the request timestamp? + */ +#define TIMESTAMP_TOLERANCE \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) + + +/** + * Closure for #reserve_attest_transaction. + */ +struct ReserveAttestContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Hash of the payto URI of this reserve. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Timestamp of the request. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Expiration time for the attestation. + */ + struct GNUNET_TIME_Timestamp etime; + + /** + * List of requested details. + */ + const json_t *details; + + /** + * Client signature approving the request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Attributes we are affirming. JSON object. + */ + json_t *json_attest; + + /** + * Database error codes encountered. + */ + enum GNUNET_DB_QueryStatus qs; + + /** + * Set to true if we did not find the reserve. + */ + bool not_found; + +}; + + +/** + * Send reserve attest to client. + * + * @param connection connection to the client + * @param rhc reserve attest to return + * @return MHD result code + */ +static MHD_RESULT +reply_reserve_attest_success (struct MHD_Connection *connection, + const struct ReserveAttestContext *rhc) +{ + struct TALER_ExchangeSignatureP exchange_sig; + struct TALER_ExchangePublicKeyP exchange_pub; + enum TALER_ErrorCode ec; + struct GNUNET_TIME_Timestamp now; + + if (NULL == rhc->json_attest) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + NULL); + } + now = GNUNET_TIME_timestamp_get (); + ec = TALER_exchange_online_reserve_attest_details_sign ( + &TEH_keys_exchange_sign_, + now, + rhc->etime, + &rhc->reserve_pub, + rhc->json_attest, + &exchange_pub, + &exchange_sig); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + now), + GNUNET_JSON_pack_timestamp ("expiration_time", + rhc->etime), + GNUNET_JSON_pack_object_steal ("attributes", + rhc->json_attest)); +} + + +/** + * Function called with information about all applicable + * legitimization processes for the given user. Finds the + * available attributes and merges them into our result + * set based on the details requested by the client. + * + * @param cls our `struct ReserveAttestContext *` + * @param h_payto account for which the attribute data is stored + * @param provider_section provider that must be checked + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + */ +static void +kyc_process_cb (void *cls, + const struct TALER_PaytoHashP *h_payto, + const char *provider_section, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes) +{ + struct ReserveAttestContext *rsc = cls; + json_t *attrs; + json_t *val; + const char *name; + bool match = false; + + if (GNUNET_TIME_absolute_is_past (expiration_time.abs_time)) + return; + attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key, + enc_attributes, + enc_attributes_size); + json_object_foreach (attrs, name, val) + { + bool requested = false; + size_t idx; + json_t *str; + + if (NULL != json_object_get (rsc->json_attest, + name)) + continue; /* duplicate */ + json_array_foreach (rsc->details, idx, str) + { + if (0 == strcmp (json_string_value (str), + name)) + { + requested = true; + break; + } + } + if (! requested) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Skipping attribute `%s': not requested\n", + name); + continue; + } + match = true; + GNUNET_assert (0 == + json_object_set (rsc->json_attest, /* NOT set_new! */ + name, + val)); + } + json_decref (attrs); + if (! match) + return; + rsc->etime = GNUNET_TIME_timestamp_min (expiration_time, + rsc->etime); +} + + +/** + * Function implementing /reserves/$RID/attest transaction. Given the public + * key of a reserve, return the associated transaction attest. Runs the + * transaction logic; IF it returns a non-error code, the transaction logic + * MUST NOT queue a MHD response. IF it returns an hard error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it + * returns the soft error code, the function MAY be called again to retry and + * MUST not queue a MHD response. + * + * @param cls a `struct ReserveAttestContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!); unused + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +reserve_attest_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveAttestContext *rsc = cls; + enum GNUNET_DB_QueryStatus qs; + + rsc->json_attest = json_object (); + GNUNET_assert (NULL != rsc->json_attest); + qs = TEH_plugin->select_kyc_attributes (TEH_plugin->cls, + &rsc->h_payto, + &kyc_process_cb, + rsc); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_kyc_attributes"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + rsc->not_found = true; + return qs; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + rsc->not_found = false; + break; + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_attest (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]) +{ + struct ReserveAttestContext rsc = { + .etime = GNUNET_TIME_UNIT_FOREVER_TS + }; + MHD_RESULT mhd_ret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rsc.timestamp), + GNUNET_JSON_spec_array_const ("details", + &rsc.details), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rsc.reserve_sig), + GNUNET_JSON_spec_end () + }; + struct GNUNET_TIME_Timestamp now; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &rsc.reserve_pub, + sizeof (rsc.reserve_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, + args[0]); + } + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, + rsc.timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } + + if (GNUNET_OK != + TALER_wallet_reserve_attest_request_verify (rsc.timestamp, + rsc.details, + &rsc.reserve_pub, + &rsc.reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE, + NULL); + } + + { + char *payto_uri; + + payto_uri = TALER_reserve_make_payto (TEH_base_url, + &rsc.reserve_pub); + TALER_payto_hash (payto_uri, + &rsc.h_payto); + GNUNET_free (payto_uri); + } + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "post reserve attest", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_attest_transaction, + &rsc)) + { + return mhd_ret; + } + if (rsc.not_found) + { + json_decref (rsc.json_attest); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + args[0]); + } + return reply_reserve_attest_success (rc->connection, + &rsc); +} + + +/* end of taler-exchange-httpd_reserves_attest.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_attest.h b/src/exchange/taler-exchange-httpd_reserves_attest.h new file mode 100644 index 000000000..66bbdf712 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_attest.h @@ -0,0 +1,41 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_attest.h + * @brief Handle /reserves/$RESERVE_PUB/attest requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_RESERVES_ATTEST_H +#define TALER_EXCHANGE_HTTPD_RESERVES_ATTEST_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a POST "/reserves-attest/$RID" request. + * + * @param rc request context + * @param root uploaded body from the client + * @param args args[0] has public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_attest (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_close.c b/src/exchange/taler-exchange-httpd_reserves_close.c new file mode 100644 index 000000000..bbf234428 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_close.c @@ -0,0 +1,448 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_close.c + * @brief Handle /reserves/$RESERVE_PUB/close requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler_json_lib.h" +#include "taler_dbevents.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_reserves_close.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How far do we allow a client's time to be off when + * checking the request timestamp? + */ +#define TIMESTAMP_TOLERANCE \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) + + +/** + * Closure for #reserve_close_transaction. + */ +struct ReserveCloseContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /** + * Timestamp of the request. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Client signature approving the request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Amount that will be wired (after closing fees). + */ + struct TALER_Amount wire_amount; + + /** + * Current balance of the reserve. + */ + struct TALER_Amount balance; + + /** + * Where to wire the funds, may be NULL. + */ + const char *payto_uri; + + /** + * Hash of the @e payto_uri, if given (otherwise zero). + */ + struct TALER_PaytoHashP h_payto; + + /** + * KYC status for the request. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Hash of the payto-URI that was used for the KYC decision. + */ + struct TALER_PaytoHashP kyc_payto; + + /** + * Query status from the amount_it() helper function. + */ + enum GNUNET_DB_QueryStatus qs; +}; + + +/** + * Send reserve close to client. + * + * @param connection connection to the client + * @param rhc reserve close to return + * @return MHD result code + */ +static MHD_RESULT +reply_reserve_close_success (struct MHD_Connection *connection, + const struct ReserveCloseContext *rhc) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("wire_amount", + &rhc->wire_amount)); +} + + +/** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and + * account to iterate over events for + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +amount_it (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct ReserveCloseContext *rcc = cls; + enum GNUNET_GenericReturnValue ret; + + ret = cb (cb_cls, + &rcc->balance, + GNUNET_TIME_absolute_get ()); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return; + rcc->qs + = TEH_plugin->iterate_reserve_close_info ( + TEH_plugin->cls, + &rcc->kyc_payto, + limit, + cb, + cb_cls); +} + + +/** + * Function implementing /reserves/$RID/close transaction. Given the public + * key of a reserve, return the associated transaction close. Runs the + * transaction logic; IF it returns a non-error code, the transaction logic + * MUST NOT queue a MHD response. IF it returns an hard error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it + * returns the soft error code, the function MAY be called again to retry and + * MUST not queue a MHD response. + * + * @param cls a `struct ReserveCloseContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!); unused + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +reserve_close_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveCloseContext *rcc = cls; + enum GNUNET_DB_QueryStatus qs; + char *payto_uri = NULL; + const struct TALER_WireFeeSet *wf; + + qs = TEH_plugin->select_reserve_close_info ( + TEH_plugin->cls, + rcc->reserve_pub, + &rcc->balance, + &payto_uri); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_reserve_close_info"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if ( (NULL == rcc->payto_uri) && + (NULL == payto_uri) ) + { + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if ( (NULL != rcc->payto_uri) && + ( (NULL == payto_uri) || + (0 != strcmp (payto_uri, + rcc->payto_uri)) ) ) + { + /* KYC check may be needed: we're not returning + the money to the account that funded the reserve + in the first place. */ + char *kyc_needed; + + TALER_payto_hash (rcc->payto_uri, + &rcc->kyc_payto); + rcc->qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE, + &rcc->kyc_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &amount_it, + rcc, + &kyc_needed); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "iterate_reserve_close_info"); + return qs; + } + if (rcc->qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == rcc->qs) + return rcc->qs; + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "iterate_reserve_close_info"); + return qs; + } + if (NULL != kyc_needed) + { + rcc->kyc.ok = false; + qs = TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + kyc_needed, + &rcc->kyc_payto, + rcc->reserve_pub, + &rcc->kyc.requirement_row); + GNUNET_free (kyc_needed); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_for_account"); + } + return qs; + } + } + + rcc->kyc.ok = true; + if (NULL == rcc->payto_uri) + rcc->payto_uri = payto_uri; + + { + char *method; + + method = TALER_payto_get_method (rcc->payto_uri); + wf = TEH_wire_fees_by_time (rcc->timestamp, + method); + if (NULL == wf) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, + method); + GNUNET_free (method); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_free (method); + } + + if (0 > + TALER_amount_subtract (&rcc->wire_amount, + &rcc->balance, + &wf->closing)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client attempted to close reserve with insufficient balance.\n"); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &rcc->wire_amount)); + *mhd_ret = reply_reserve_close_success (connection, + rcc); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->insert_close_request (TEH_plugin->cls, + rcc->reserve_pub, + payto_uri, + &rcc->reserve_sig, + rcc->timestamp, + &rcc->balance, + &wf->closing); + GNUNET_free (payto_uri); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "insert_close_request"); + return qs; + } + if (qs <= 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_close (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct ReserveCloseContext rcc = { + .payto_uri = NULL, + .reserve_pub = reserve_pub + }; + MHD_RESULT mhd_ret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rcc.timestamp), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_payto_uri ("payto_uri", + &rcc.payto_uri), + NULL), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rcc.reserve_sig), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + + { + struct GNUNET_TIME_Timestamp now; + + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, + rcc.timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } + } + + if (NULL != rcc.payto_uri) + TALER_payto_hash (rcc.payto_uri, + &rcc.h_payto); + if (GNUNET_OK != + TALER_wallet_reserve_close_verify (rcc.timestamp, + &rcc.h_payto, + reserve_pub, + &rcc.reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE, + NULL); + } + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "reserve close", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_close_transaction, + &rcc)) + { + return mhd_ret; + } + if (! rcc.kyc.ok) + return TEH_RESPONSE_reply_kyc_required (rc->connection, + &rcc.kyc_payto, + &rcc.kyc); + + return reply_reserve_close_success (rc->connection, + &rcc); +} + + +/* end of taler-exchange-httpd_reserves_close.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_close.h b/src/exchange/taler-exchange-httpd_reserves_close.h new file mode 100644 index 000000000..4c70b17cb --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_close.h @@ -0,0 +1,41 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_close.h + * @brief Handle /reserves/$RESERVE_PUB/close requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_RESERVES_CLOSE_H +#define TALER_EXCHANGE_HTTPD_RESERVES_CLOSE_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a POST "/reserves/$RID/close" request. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @param root uploaded body from the client + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_close (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_get.c b/src/exchange/taler-exchange-httpd_reserves_get.c index 80c992e61..0775a4c65 100644 --- a/src/exchange/taler-exchange-httpd_reserves_get.c +++ b/src/exchange/taler-exchange-httpd_reserves_get.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 Affero General Public License as published by the Free Software @@ -52,8 +52,12 @@ struct ReservePoller struct MHD_Connection *connection; /** - * Subscription for the database event we are - * waiting for. + * Our request context. + */ + struct TEH_RequestContext *rc; + + /** + * Subscription for the database event we are waiting for. */ struct GNUNET_DB_EventHandler *eh; @@ -63,6 +67,16 @@ struct ReservePoller struct GNUNET_TIME_Absolute timeout; /** + * Public key of the reserve the inquiry is about. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Balance of the reserve, set in the callback. + */ + struct TALER_Amount balance; + + /** * True if we are still suspended. */ bool suspended; @@ -84,13 +98,10 @@ static struct ReservePoller *rp_tail; void TEH_reserves_get_cleanup () { - struct ReservePoller *rp; - - while (NULL != (rp = rp_head)) + for (struct ReservePoller *rp = rp_head; + NULL != rp; + rp = rp->next) { - GNUNET_CONTAINER_DLL_remove (rp_head, - rp_tail, - rp); if (rp->suspended) { rp->suspended = false; @@ -115,11 +126,14 @@ rp_cleanup (struct TEH_RequestContext *rc) if (NULL != rp->eh) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Cancelling DB event listening\n"); + "Cancelling DB event listening on cleanup (odd unless during shutdown)\n"); TEH_plugin->event_listen_cancel (TEH_plugin->cls, rp->eh); rp->eh = NULL; } + GNUNET_CONTAINER_DLL_remove (rp_head, + rp_tail, + rp); GNUNET_free (rp); } @@ -137,26 +151,17 @@ db_event_cb (void *cls, const void *extra, size_t extra_size) { - struct TEH_RequestContext *rc = cls; - struct ReservePoller *rp = rc->rh_ctx; + struct ReservePoller *rp = cls; struct GNUNET_AsyncScopeSave old_scope; (void) extra; (void) extra_size; - if (NULL == rp) - return; /* event triggered while main transaction - was still running */ if (! rp->suspended) return; /* might get multiple wake-up events */ - rp->suspended = false; - GNUNET_async_scope_enter (&rc->async_scope_id, + GNUNET_async_scope_enter (&rp->rc->async_scope_id, &old_scope); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming from long-polling on reserve\n"); TEH_check_invariants (); - GNUNET_CONTAINER_DLL_remove (rp_head, - rp_tail, - rp); + rp->suspended = false; MHD_resume_connection (rp->connection); TALER_MHD_daemon_trigger (); TEH_check_invariants (); @@ -164,205 +169,95 @@ db_event_cb (void *cls, } -/** - * Send reserve history to client. - * - * @param connection connection to the client - * @param rh reserve history to return - * @return MHD result code - */ -static MHD_RESULT -reply_reserve_history_success (struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_ReserveHistory *rh) -{ - json_t *json_history; - struct TALER_Amount balance; - - json_history = TEH_RESPONSE_compile_reserve_history (rh, - &balance); - if (NULL == json_history) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - NULL); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("balance", - &balance), - GNUNET_JSON_pack_array_steal ("history", - json_history)); -} - - -/** - * Closure for #reserve_history_transaction. - */ -struct ReserveHistoryContext -{ - /** - * Public key of the reserve the inquiry is about. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * History of the reserve, set in the callback. - */ - struct TALER_EXCHANGEDB_ReserveHistory *rh; - -}; - - -/** - * Function implementing /reserves/ GET transaction. - * Execute a /reserves/ GET. Given the public key of a reserve, - * return the associated transaction history. Runs the - * transaction logic; IF it returns a non-error code, the transaction - * logic MUST NOT queue a MHD response. IF it returns an hard error, - * the transaction logic MUST queue a MHD response and set @a mhd_ret. - * IF it returns the soft error code, the function MAY be called again - * to retry and MUST not queue a MHD response. - * - * @param cls a `struct ReserveHistoryContext *` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!); unused - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -reserve_history_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct ReserveHistoryContext *rsc = cls; - struct TALER_Amount balance; - - (void) connection; - (void) mhd_ret; - return TEH_plugin->get_reserve_history (TEH_plugin->cls, - &rsc->reserve_pub, - &balance, - &rsc->rh); -} - - MHD_RESULT -TEH_handler_reserves_get (struct TEH_RequestContext *rc, - const char *const args[1]) +TEH_handler_reserves_get ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub) { - struct ReserveHistoryContext rsc; - MHD_RESULT mhd_ret; - struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; - struct GNUNET_DB_EventHandler *eh = NULL; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &rsc.reserve_pub, - sizeof (rsc.reserve_pub))) + struct ReservePoller *rp = rc->rh_ctx; + + if (NULL == rp) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_GENERIC_RESERVE_PUB_MALFORMED, - args[0]); + rp = GNUNET_new (struct ReservePoller); + rp->connection = rc->connection; + rp->rc = rc; + rc->rh_ctx = rp; + rc->rh_cleaner = &rp_cleanup; + GNUNET_CONTAINER_DLL_insert (rp_head, + rp_tail, + rp); + rp->reserve_pub = *reserve_pub; + TALER_MHD_parse_request_timeout (rc->connection, + &rp->timeout); } - { - const char *long_poll_timeout_ms; - long_poll_timeout_ms - = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, - "timeout_ms"); - if (NULL != long_poll_timeout_ms) - { - unsigned int timeout_ms; - char dummy; - - if (1 != sscanf (long_poll_timeout_ms, - "%u%c", - &timeout_ms, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "timeout_ms (must be non-negative number)"); - } - timeout = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, - timeout_ms); - } - } - if ( (! GNUNET_TIME_relative_is_zero (timeout)) && - (NULL == rc->rh_ctx) ) + if ( (GNUNET_TIME_absolute_is_future (rp->timeout)) && + (NULL == rp->eh) ) { struct TALER_ReserveEventP rep = { .header.size = htons (sizeof (rep)), .header.type = htons (TALER_DBEVENT_EXCHANGE_RESERVE_INCOMING), - .reserve_pub = rsc.reserve_pub + .reserve_pub = rp->reserve_pub }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting DB event listening\n"); - eh = TEH_plugin->event_listen (TEH_plugin->cls, - timeout, - &rep.header, - &db_event_cb, - rc); - } - rsc.rh = NULL; - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "get reserve history", - TEH_MT_OTHER, - &mhd_ret, - &reserve_history_transaction, - &rsc)) - { - if (NULL != eh) - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - eh); - return mhd_ret; + "Starting DB event listening until %s\n", + GNUNET_TIME_absolute2s (rp->timeout)); + rp->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (rp->timeout), + &rep.header, + &db_event_cb, + rp); } - /* generate proper response */ - if (NULL == rsc.rh) { - struct ReservePoller *rp = rc->rh_ctx; + enum GNUNET_DB_QueryStatus qs; - if ( (NULL != rp) || - (GNUNET_TIME_relative_is_zero (timeout)) ) + qs = TEH_plugin->get_reserve_balance (TEH_plugin->cls, + &rp->reserve_pub, + &rp->balance); + switch (qs) { + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); /* single-shot query should never have soft-errors */ return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_RESERVES_GET_STATUS_UNKNOWN, - args[0]); + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_reserve_balance"); + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_reserve_balance"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got reserve balance of %s\n", + TALER_amount2s (&rp->balance)); + return TALER_MHD_REPLY_JSON_PACK (rc->connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("balance", + &rp->balance)); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (! GNUNET_TIME_absolute_is_future (rp->timeout)) + { + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Long-polling on reserve for %s\n", + GNUNET_STRINGS_relative_time_to_string ( + GNUNET_TIME_absolute_get_remaining (rp->timeout), + true)); + rp->suspended = true; + MHD_suspend_connection (rc->connection); + return MHD_YES; } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Long-polling on reserve for %s\n", - GNUNET_STRINGS_relative_time_to_string (timeout, - GNUNET_YES)); - rp = GNUNET_new (struct ReservePoller); - rp->connection = rc->connection; - rp->timeout = GNUNET_TIME_relative_to_absolute (timeout); - rp->eh = eh; - rc->rh_ctx = rp; - rc->rh_cleaner = &rp_cleanup; - rp->suspended = true; - GNUNET_CONTAINER_DLL_insert (rp_head, - rp_tail, - rp); - MHD_suspend_connection (rc->connection); - return MHD_YES; } - if (NULL != eh) - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - eh); - mhd_ret = reply_reserve_history_success (rc->connection, - rsc.rh); - TEH_plugin->free_reserve_history (TEH_plugin->cls, - rsc.rh); - return mhd_ret; + GNUNET_break (0); + return MHD_NO; } diff --git a/src/exchange/taler-exchange-httpd_reserves_get.h b/src/exchange/taler-exchange-httpd_reserves_get.h index 30c6559f6..6c453d0cd 100644 --- a/src/exchange/taler-exchange-httpd_reserves_get.h +++ b/src/exchange/taler-exchange-httpd_reserves_get.h @@ -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 Affero General Public License as published by the Free Software @@ -43,11 +43,12 @@ TEH_reserves_get_cleanup (void); * status of the reserve. * * @param rc request context - * @param args array of additional options (length: 1, just the reserve_pub) + * @param reserve_pub public key of the reserve * @return MHD result code */ MHD_RESULT -TEH_handler_reserves_get (struct TEH_RequestContext *rc, - const char *const args[1]); +TEH_handler_reserves_get ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub); #endif diff --git a/src/exchange/taler-exchange-httpd_reserves_get_attest.c b/src/exchange/taler-exchange-httpd_reserves_get_attest.c new file mode 100644 index 000000000..ae983682a --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_get_attest.c @@ -0,0 +1,232 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_get_attest.c + * @brief Handle GET /reserves/$RESERVE_PUB/attest requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler_json_lib.h" +#include "taler_dbevents.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_reserves_get_attest.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for #reserve_attest_transaction. + */ +struct ReserveAttestContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Hash of the payto URI of this reserve. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Available attributes. + */ + json_t *attributes; + + /** + * Set to true if we did not find the reserve. + */ + bool not_found; +}; + + +/** + * Function called with information about all applicable + * legitimization processes for the given user. + * + * @param cls our `struct ReserveAttestContext *` + * @param h_payto account for which the attribute data is stored + * @param provider_section provider that must be checked + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + */ +static void +kyc_process_cb (void *cls, + const struct TALER_PaytoHashP *h_payto, + const char *provider_section, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes) +{ + struct ReserveAttestContext *rsc = cls; + json_t *attrs; + json_t *val; + const char *name; + + if (GNUNET_TIME_absolute_is_past (expiration_time.abs_time)) + return; + attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key, + enc_attributes, + enc_attributes_size); + json_object_foreach (attrs, name, val) + { + bool duplicate = false; + size_t idx; + json_t *str; + + json_array_foreach (rsc->attributes, idx, str) + { + if (0 == strcmp (json_string_value (str), + name)) + { + duplicate = true; + break; + } + } + if (duplicate) + continue; + GNUNET_assert (0 == + json_array_append (rsc->attributes, + json_string (name))); + } +} + + +/** + * Function implementing GET /reserves/$RID/attest transaction. + * Execute a /reserves/ get attest. Given the public key of a reserve, + * return the associated transaction attest. Runs the + * transaction logic; IF it returns a non-error code, the transaction + * logic MUST NOT queue a MHD response. IF it returns an hard error, + * the transaction logic MUST queue a MHD response and set @a mhd_ret. + * IF it returns the soft error code, the function MAY be called again + * to retry and MUST not queue a MHD response. + * + * @param cls a `struct ReserveAttestContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +reserve_attest_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveAttestContext *rsc = cls; + enum GNUNET_DB_QueryStatus qs; + + rsc->attributes = json_array (); + GNUNET_assert (NULL != rsc->attributes); + qs = TEH_plugin->select_kyc_attributes (TEH_plugin->cls, + &rsc->h_payto, + &kyc_process_cb, + rsc); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "iterate_kyc_reference"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + rsc->not_found = true; + return qs; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + rsc->not_found = false; + break; + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_get_attest (struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct ReserveAttestContext rsc = { + .attributes = NULL + }; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &rsc.reserve_pub, + sizeof (rsc.reserve_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, + args[0]); + } + { + char *payto_uri; + + payto_uri = TALER_reserve_make_payto (TEH_base_url, + &rsc.reserve_pub); + TALER_payto_hash (payto_uri, + &rsc.h_payto); + GNUNET_free (payto_uri); + } + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "get-attestable", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_attest_transaction, + &rsc)) + { + json_decref (rsc.attributes); + rsc.attributes = NULL; + return mhd_ret; + } + } + /* generate proper response */ + if (rsc.not_found) + { + json_decref (rsc.attributes); + rsc.attributes = NULL; + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + args[0]); + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("details", + rsc.attributes)); +} + + +/* end of taler-exchange-httpd_reserves_get_attest.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_get_attest.h b/src/exchange/taler-exchange-httpd_reserves_get_attest.h new file mode 100644 index 000000000..8b5e3aba3 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_get_attest.h @@ -0,0 +1,44 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_get_attest.h + * @brief Handle /reserves/$RESERVE_PUB GET_ATTEST requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_RESERVES_GET_ATTEST_H +#define TALER_EXCHANGE_HTTPD_RESERVES_GET_ATTEST_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/reserves/$RID/attest" request. Parses the + * given "reserve_pub" in @a args (which should contain the + * EdDSA public key of a reserve) and then responds with the + * available attestations for the reserve. + * + * @param rc request context + * @param args array of additional options (length: 1, just the reserve_pub) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_get_attest (struct TEH_RequestContext *rc, + const char *const args[1]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_history.c b/src/exchange/taler-exchange-httpd_reserves_history.c new file mode 100644 index 000000000..056d4b0ef --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_history.c @@ -0,0 +1,517 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_history.c + * @brief Handle /reserves/$RESERVE_PUB HISTORY requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_json_lib.h" +#include "taler_dbevents.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_reserves_history.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Compile the history of a reserve into a JSON object. + * + * @param rh reserve history to JSON-ify + * @return json representation of the @a rh, NULL on error + */ +static json_t * +compile_reserve_history ( + const struct TALER_EXCHANGEDB_ReserveHistory *rh) +{ + json_t *json_history; + + json_history = json_array (); + GNUNET_assert (NULL != json_history); + for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh; + NULL != pos; + pos = pos->next) + { + switch (pos->type) + { + case TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE: + { + const struct TALER_EXCHANGEDB_BankTransfer *bank = + pos->details.bank; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "CREDIT"), + GNUNET_JSON_pack_timestamp ("timestamp", + bank->execution_date), + GNUNET_JSON_pack_string ("sender_account_url", + bank->sender_account_details), + GNUNET_JSON_pack_uint64 ("wire_reference", + bank->wire_reference), + TALER_JSON_pack_amount ("amount", + &bank->amount)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + break; + } + case TALER_EXCHANGEDB_RO_WITHDRAW_COIN: + { + const struct TALER_EXCHANGEDB_CollectableBlindcoin *withdraw + = pos->details.withdraw; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "WITHDRAW"), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &withdraw->reserve_sig), + GNUNET_JSON_pack_data_auto ("h_coin_envelope", + &withdraw->h_coin_envelope), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &withdraw->denom_pub_hash), + TALER_JSON_pack_amount ("withdraw_fee", + &withdraw->withdraw_fee), + TALER_JSON_pack_amount ("amount", + &withdraw->amount_with_fee)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_RO_RECOUP_COIN: + { + const struct TALER_EXCHANGEDB_Recoup *recoup + = pos->details.recoup; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_sign ( + &TEH_keys_exchange_sign_, + recoup->timestamp, + &recoup->value, + &recoup->coin.coin_pub, + &recoup->reserve_pub, + &pub, + &sig)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RECOUP"), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_timestamp ("timestamp", + recoup->timestamp), + TALER_JSON_pack_amount ("amount", + &recoup->value), + GNUNET_JSON_pack_data_auto ("coin_pub", + &recoup->coin.coin_pub)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_RO_EXCHANGE_TO_BANK: + { + const struct TALER_EXCHANGEDB_ClosingTransfer *closing = + pos->details.closing; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + if (TALER_EC_NONE != + TALER_exchange_online_reserve_closed_sign ( + &TEH_keys_exchange_sign_, + closing->execution_date, + &closing->amount, + &closing->closing_fee, + closing->receiver_account_details, + &closing->wtid, + &pos->details.closing->reserve_pub, + &pub, + &sig)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "CLOSING"), + GNUNET_JSON_pack_string ("receiver_account_details", + closing->receiver_account_details), + GNUNET_JSON_pack_data_auto ("wtid", + &closing->wtid), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_timestamp ("timestamp", + closing->execution_date), + TALER_JSON_pack_amount ("amount", + &closing->amount), + TALER_JSON_pack_amount ("closing_fee", + &closing->closing_fee)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_RO_PURSE_MERGE: + { + const struct TALER_EXCHANGEDB_PurseMerge *merge = + pos->details.merge; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "MERGE"), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &merge->h_contract_terms), + GNUNET_JSON_pack_data_auto ("merge_pub", + &merge->merge_pub), + GNUNET_JSON_pack_uint64 ("min_age", + merge->min_age), + GNUNET_JSON_pack_uint64 ("flags", + merge->flags), + GNUNET_JSON_pack_data_auto ("purse_pub", + &merge->purse_pub), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &merge->reserve_sig), + GNUNET_JSON_pack_timestamp ("merge_timestamp", + merge->merge_timestamp), + GNUNET_JSON_pack_timestamp ("purse_expiration", + merge->purse_expiration), + TALER_JSON_pack_amount ("purse_fee", + &merge->purse_fee), + TALER_JSON_pack_amount ("amount", + &merge->amount_with_fee), + GNUNET_JSON_pack_bool ("merged", + merge->merged)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_RO_HISTORY_REQUEST: + { + const struct TALER_EXCHANGEDB_HistoryRequest *history = + pos->details.history; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "HISTORY"), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &history->reserve_sig), + GNUNET_JSON_pack_timestamp ("request_timestamp", + history->request_timestamp), + TALER_JSON_pack_amount ("amount", + &history->history_fee)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + + case TALER_EXCHANGEDB_RO_OPEN_REQUEST: + { + const struct TALER_EXCHANGEDB_OpenRequest *orq = + pos->details.open_request; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "OPEN"), + GNUNET_JSON_pack_uint64 ("requested_min_purses", + orq->purse_limit), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &orq->reserve_sig), + GNUNET_JSON_pack_timestamp ("request_timestamp", + orq->request_timestamp), + GNUNET_JSON_pack_timestamp ("requested_expiration", + orq->reserve_expiration), + TALER_JSON_pack_amount ("open_fee", + &orq->open_fee)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + + case TALER_EXCHANGEDB_RO_CLOSE_REQUEST: + { + const struct TALER_EXCHANGEDB_CloseRequest *crq = + pos->details.close_request; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "CLOSE"), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &crq->reserve_sig), + GNUNET_is_zero (&crq->target_account_h_payto) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("h_payto", + NULL)) + : GNUNET_JSON_pack_data_auto ("h_payto", + &crq->target_account_h_payto), + GNUNET_JSON_pack_timestamp ("request_timestamp", + crq->request_timestamp)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + } + } + + return json_history; +} + + +/** + * Add the headers we want to set for every /keys response. + * + * @param cls the key state to use + * @param[in,out] response the response to modify + */ +static void +add_response_headers (void *cls, + struct MHD_Response *response) +{ + (void) cls; + TALER_MHD_add_global_headers (response); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-cache")); +} + + +MHD_RESULT +TEH_handler_reserves_history ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub) +{ + struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; + uint64_t start_off = 0; + struct TALER_Amount balance; + uint64_t etag_in; + uint64_t etag_out; + char etagp[24]; + struct MHD_Response *resp; + unsigned int http_status; + + TALER_MHD_parse_request_number (rc->connection, + "start", + &start_off); + { + struct TALER_ReserveSignatureP reserve_sig; + bool required = true; + + TALER_MHD_parse_request_header_auto (rc->connection, + TALER_RESERVE_HISTORY_SIGNATURE_HEADER, + &reserve_sig, + required); + + if (GNUNET_OK != + TALER_wallet_reserve_history_verify (start_off, + reserve_pub, + &reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE, + NULL); + } + } + + /* Get etag */ + { + const char *etags; + + etags = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != etags) + { + char dummy; + unsigned long long ev; + + if (1 != sscanf (etags, + "\"%llu\"%c", + &ev, + &dummy)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client send malformed `If-None-Match' header `%s'\n", + etags); + etag_in = 0; + } + else + { + etag_in = (uint64_t) ev; + } + } + else + { + etag_in = start_off; + } + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_reserve_history (TEH_plugin->cls, + reserve_pub, + start_off, + etag_in, + &etag_out, + &balance, + &rh); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_reserve_history"); + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_reserve_history"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Handled below */ + break; + } + } + + GNUNET_snprintf (etagp, + sizeof (etagp), + "\"%llu\"", + (unsigned long long) etag_out); + if (etag_in == etag_out) + { + return TEH_RESPONSE_reply_not_modified (rc->connection, + etagp, + &add_response_headers, + NULL); + } + if (NULL == rh) + { + /* 204: empty history */ + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + http_status = MHD_HTTP_NO_CONTENT; + } + else + { + json_t *history; + + http_status = MHD_HTTP_OK; + history = compile_reserve_history (rh); + TEH_plugin->free_reserve_history (TEH_plugin->cls, + rh); + rh = NULL; + if (NULL == history) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + NULL); + } + resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_amount ("balance", + &balance), + GNUNET_JSON_pack_array_steal ("history", + history)); + } + add_response_headers (NULL, + resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etagp)); + { + MHD_RESULT ret; + + ret = MHD_queue_response (rc->connection, + http_status, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } +} + + +/* end of taler-exchange-httpd_reserves_history.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_history.h b/src/exchange/taler-exchange-httpd_reserves_history.h new file mode 100644 index 000000000..e1bd7ae1b --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_history.h @@ -0,0 +1,43 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_history.h + * @brief Handle /reserves/$RESERVE_PUB/history requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_RESERVES_HISTORY_H +#define TALER_EXCHANGE_HTTPD_RESERVES_HISTORY_H + +#include <microhttpd.h> +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/reserves/$RID/history" request. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_history ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub); + +#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_open.c b/src/exchange/taler-exchange-httpd_reserves_open.c new file mode 100644 index 000000000..5aadc9e40 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_open.c @@ -0,0 +1,471 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_open.c + * @brief Handle /reserves/$RESERVE_PUB/open requests + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_mhd_lib.h" +#include "taler_json_lib.h" +#include "taler_dbevents.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_reserves_open.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How far do we allow a client's time to be off when + * checking the request timestamp? + */ +#define TIMESTAMP_TOLERANCE \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) + + +/** + * Closure for #reserve_open_transaction. + */ +struct ReserveOpenContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /** + * Desired (minimum) expiration time for the reserve. + */ + struct GNUNET_TIME_Timestamp desired_expiration; + + /** + * Actual expiration time for the reserve. + */ + struct GNUNET_TIME_Timestamp reserve_expiration; + + /** + * Timestamp of the request. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Client signature approving the request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Global fees applying to the request. + */ + const struct TEH_GlobalFee *gf; + + /** + * Amount to be paid from the reserve. + */ + struct TALER_Amount reserve_payment; + + /** + * Actual cost to open the reserve. + */ + struct TALER_Amount open_cost; + + /** + * Total amount that was deposited. + */ + struct TALER_Amount total; + + /** + * Information about payments by coin. + */ + struct TEH_PurseDepositedCoin *payments; + + /** + * Length of the @e payments array. + */ + unsigned int payments_len; + + /** + * Desired minimum purse limit. + */ + uint32_t purse_limit; + + /** + * Set to true if the reserve balance is too low + * for the operation. + */ + bool no_funds; + +}; + + +/** + * Send reserve open to client. + * + * @param connection connection to the client + * @param rsc reserve open data to return + * @return MHD result code + */ +static MHD_RESULT +reply_reserve_open_success (struct MHD_Connection *connection, + const struct ReserveOpenContext *rsc) +{ + struct GNUNET_TIME_Timestamp now; + struct GNUNET_TIME_Timestamp re; + unsigned int status; + + status = MHD_HTTP_OK; + if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, + <, + rsc->desired_expiration)) + status = MHD_HTTP_PAYMENT_REQUIRED; + now = GNUNET_TIME_timestamp_get (); + if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, + <, + now)) + re = now; + else + re = rsc->reserve_expiration; + return TALER_MHD_REPLY_JSON_PACK ( + connection, + status, + GNUNET_JSON_pack_timestamp ("reserve_expiration", + re), + TALER_JSON_pack_amount ("open_cost", + &rsc->open_cost)); +} + + +/** + * Cleans up information in @a rsc, but does not + * free @a rsc itself (allocated on the stack!). + * + * @param[in] rsc struct with information to clean up + */ +static void +cleanup_rsc (struct ReserveOpenContext *rsc) +{ + for (unsigned int i = 0; i<rsc->payments_len; i++) + { + TEH_common_purse_deposit_free_coin (&rsc->payments[i]); + } + GNUNET_free (rsc->payments); +} + + +/** + * Function implementing /reserves/$RID/open transaction. Given the public + * key of a reserve, return the associated transaction open. Runs the + * transaction logic; IF it returns a non-error code, the transaction logic + * MUST NOT queue a MHD response. IF it returns an hard error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it + * returns the soft error code, the function MAY be called again to retry and + * MUST not queue a MHD response. + * + * @param cls a `struct ReserveOpenContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +reserve_open_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveOpenContext *rsc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount reserve_balance; + + for (unsigned int i = 0; i<rsc->payments_len; i++) + { + struct TEH_PurseDepositedCoin *coin = &rsc->payments[i]; + bool insufficient_funds = true; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Make coin %u known\n", + i); + qs = TEH_make_coin_known (&coin->cpi, + connection, + &coin->known_coin_id, + mhd_ret); + if (qs < 0) + return qs; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Insert open deposit %u known\n", + i); + qs = TEH_plugin->insert_reserve_open_deposit ( + TEH_plugin->cls, + &coin->cpi, + &coin->coin_sig, + coin->known_coin_id, + &coin->amount, + &rsc->reserve_sig, + rsc->reserve_pub, + &insufficient_funds); + /* 0 == qs is fine, then the coin was already + spent for this very operation as identified + by reserve_sig! */ + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_reserve_open_deposit"); + return qs; + } + if (insufficient_funds) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handle insufficient funds\n"); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &coin->cpi.denom_pub_hash, + &coin->cpi.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Do reserve open with reserve payment of %s\n", + TALER_amount2s (&rsc->total)); + qs = TEH_plugin->do_reserve_open (TEH_plugin->cls, + /* inputs */ + rsc->reserve_pub, + &rsc->total, + &rsc->reserve_payment, + rsc->purse_limit, + &rsc->reserve_sig, + rsc->desired_expiration, + rsc->timestamp, + &rsc->gf->fees.account, + /* outputs */ + &rsc->no_funds, + &reserve_balance, + &rsc->open_cost, + &rsc->reserve_expiration); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_reserve_open"); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + if (rsc->no_funds) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TEH_RESPONSE_reply_reserve_insufficient_balance ( + connection, + TALER_EC_EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS, + &reserve_balance, + &rsc->reserve_payment, + rsc->reserve_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_open (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct ReserveOpenContext rsc; + const json_t *payments; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rsc.timestamp), + GNUNET_JSON_spec_timestamp ("reserve_expiration", + &rsc.desired_expiration), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rsc.reserve_sig), + GNUNET_JSON_spec_uint32 ("purse_limit", + &rsc.purse_limit), + GNUNET_JSON_spec_array_const ("payments", + &payments), + TALER_JSON_spec_amount ("reserve_payment", + TEH_currency, + &rsc.reserve_payment), + GNUNET_JSON_spec_end () + }; + + rsc.reserve_pub = reserve_pub; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + + { + struct GNUNET_TIME_Timestamp now; + + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, + rsc.timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } + } + + rsc.payments_len = json_array_size (payments); + rsc.payments = GNUNET_new_array (rsc.payments_len, + struct TEH_PurseDepositedCoin); + rsc.total = rsc.reserve_payment; + for (unsigned int i = 0; i<rsc.payments_len; i++) + { + struct TEH_PurseDepositedCoin *coin = &rsc.payments[i]; + enum GNUNET_GenericReturnValue res; + + res = TEH_common_purse_deposit_parse_coin ( + rc->connection, + coin, + json_array_get (payments, + i)); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + cleanup_rsc (&rsc); + return MHD_YES; /* failure */ + } + if (0 > + TALER_amount_add (&rsc.total, + &rsc.total, + &coin->amount_minus_fee)) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + NULL); + } + } + + { + struct TEH_KeyStateHandle *keys; + + keys = TEH_keys_get_state (); + if (NULL == keys) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + rsc.gf = TEH_keys_global_fee_by_time (keys, + rsc.timestamp); + } + if (NULL == rsc.gf) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + NULL); + } + + if (GNUNET_OK != + TALER_wallet_reserve_open_verify (&rsc.reserve_payment, + rsc.timestamp, + rsc.desired_expiration, + rsc.purse_limit, + reserve_pub, + &rsc.reserve_sig)) + { + GNUNET_break_op (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE, + NULL); + } + + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "reserve open", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_open_transaction, + &rsc)) + { + cleanup_rsc (&rsc); + return mhd_ret; + } + } + + { + MHD_RESULT mhd_ret; + + mhd_ret = reply_reserve_open_success (rc->connection, + &rsc); + cleanup_rsc (&rsc); + return mhd_ret; + } +} + + +/* end of taler-exchange-httpd_reserves_open.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_open.h b/src/exchange/taler-exchange-httpd_reserves_open.h new file mode 100644 index 000000000..e28c22c0b --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_open.h @@ -0,0 +1,41 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_open.h + * @brief Handle /reserves/$RESERVE_PUB/open requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_RESERVES_OPEN_H +#define TALER_EXCHANGE_HTTPD_RESERVES_OPEN_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a POST "/reserves/$RID/open" request. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @param root uploaded body from the client + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_open (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_purse.c b/src/exchange/taler-exchange-httpd_reserves_purse.c new file mode 100644 index 000000000..5e06db206 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_purse.c @@ -0,0 +1,774 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_purse.c + * @brief Handle /reserves/$RID/purse requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_reserves_purse.h" +#include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_lib.h" +#include "taler-exchange-httpd_keys.h" + + +/** + * Closure for #purse_transaction. + */ +struct ReservePurseContext +{ + + /** + * Public key of the reserve we are creating a purse for. + */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /** + * Fees for the operation. + */ + const struct TEH_GlobalFee *gf; + + /** + * Signature of the reserve affirming the merge. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Purse fee the client is willing to pay. + */ + struct TALER_Amount purse_fee; + + /** + * Total amount already put into the purse. + */ + struct TALER_Amount deposit_total; + + /** + * Merge time. + */ + struct GNUNET_TIME_Timestamp merge_timestamp; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Details about an encrypted contract, if any. + */ + struct TALER_EncryptedContract econtract; + + /** + * Merge key for the purse. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Merge affirmation by the @e merge_pub. + */ + struct TALER_PurseMergeSignatureP merge_sig; + + /** + * Signature of the client affiming this request. + */ + struct TALER_PurseContractSignatureP purse_sig; + + /** + * Fundamental details about the purse. + */ + struct TEH_PurseDetails pd; + + /** + * Hash of the @e payto_uri. + */ + struct TALER_PaytoHashP h_payto; + + /** + * KYC status of the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Minimum age for deposits into this purse. + */ + uint32_t min_age; + + /** + * Flags for the operation. + */ + enum TALER_WalletAccountMergeFlags flags; + + /** + * Do we lack an @e econtract? + */ + bool no_econtract; + +}; + + +/** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls a `struct ReservePurseContext` + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +amount_iterator (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct ReservePurseContext *rpc = cls; + enum GNUNET_DB_QueryStatus qs; + + cb (cb_cls, + &rpc->deposit_total, + GNUNET_TIME_absolute_get ()); + qs = TEH_plugin->select_merge_amounts_for_kyc_check ( + TEH_plugin->cls, + &rpc->h_payto, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this merge and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); +} + + +/** + * Execute database transaction for /reserves/$PID/purse. Runs the transaction + * logic; IF it returns a non-error code, the transaction logic MUST NOT queue + * a MHD response. IF it returns an hard error, the transaction logic MUST + * queue a MHD response and set @a mhd_ret. IF it returns the soft error + * code, the function MAY be called again to retry and MUST not queue a MHD + * response. + * + * @param cls a `struct ReservePurseContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +purse_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReservePurseContext *rpc = cls; + enum GNUNET_DB_QueryStatus qs; + char *required; + + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, + &rpc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &amount_iterator, + rpc, + &required); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "kyc_test_required"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (NULL != required) + { + rpc->kyc.ok = false; + qs = TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + required, + &rpc->h_payto, + rpc->reserve_pub, + &rpc->kyc.requirement_row); + GNUNET_free (required); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_for_account"); + } + return qs; + } + rpc->kyc.ok = true; + + { + bool in_conflict = true; + + /* 1) store purse */ + qs = TEH_plugin->insert_purse_request ( + TEH_plugin->cls, + &rpc->pd.purse_pub, + &rpc->merge_pub, + rpc->pd.purse_expiration, + &rpc->pd.h_contract_terms, + rpc->min_age, + rpc->flags, + &rpc->purse_fee, + &rpc->pd.target_amount, + &rpc->purse_sig, + &in_conflict); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert purse request"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return qs; + if (in_conflict) + { + struct TALER_PurseMergePublicKeyP merge_pub; + struct GNUNET_TIME_Timestamp purse_expiration; + struct TALER_PrivateContractHashP h_contract_terms; + struct TALER_Amount target_amount; + struct TALER_Amount balance; + struct TALER_PurseContractSignatureP purse_sig; + uint32_t min_age; + + TEH_plugin->rollback (TEH_plugin->cls); + qs = TEH_plugin->get_purse_request ( + TEH_plugin->cls, + &rpc->pd.purse_pub, + &merge_pub, + &purse_expiration, + &h_contract_terms, + &min_age, + &target_amount, + &balance, + &purse_sig); + if (qs <= 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (0 != qs); + TALER_LOG_WARNING ("Failed to fetch purse information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA), + TALER_JSON_pack_amount ("amount", + &target_amount), + GNUNET_JSON_pack_uint64 ("min_age", + min_age), + GNUNET_JSON_pack_timestamp ("purse_expiration", + purse_expiration), + GNUNET_JSON_pack_data_auto ("purse_sig", + &purse_sig), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &h_contract_terms), + GNUNET_JSON_pack_data_auto ("merge_pub", + &merge_pub)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + } + + /* 2) create purse with reserve (and debit reserve for purse creation!) */ + { + bool in_conflict = true; + bool insufficient_funds = true; + bool no_reserve = true; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating purse with flags %d\n", + rpc->flags); + qs = TEH_plugin->do_reserve_purse ( + TEH_plugin->cls, + &rpc->pd.purse_pub, + &rpc->merge_sig, + rpc->merge_timestamp, + &rpc->reserve_sig, + (TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA + == rpc->flags) + ? NULL + : &rpc->gf->fees.purse, + rpc->reserve_pub, + &in_conflict, + &no_reserve, + &insufficient_funds); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ( + "Failed to store purse merge information in database\n"); + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "do reserve purse"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (in_conflict) + { + /* same purse already merged into a different reserve!? */ + struct TALER_PurseContractPublicKeyP purse_pub; + struct TALER_PurseMergeSignatureP merge_sig; + struct GNUNET_TIME_Timestamp merge_timestamp; + char *partner_url; + struct TALER_ReservePublicKeyP reserve_pub; + bool refunded; + + TEH_plugin->rollback (TEH_plugin->cls); + qs = TEH_plugin->select_purse_merge ( + TEH_plugin->cls, + &purse_pub, + &merge_sig, + &merge_timestamp, + &partner_url, + &reserve_pub, + &refunded); + if (qs <= 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (0 != qs); + TALER_LOG_WARNING ( + "Failed to fetch purse merge information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse merge"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (refunded) + { + /* This is a bit of a strange case ... */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Purse was already refunded\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_GONE, + TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, + NULL); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA), + GNUNET_JSON_pack_string ("partner_url", + NULL == partner_url + ? TEH_base_url + : partner_url), + GNUNET_JSON_pack_timestamp ("merge_timestamp", + merge_timestamp), + GNUNET_JSON_pack_data_auto ("merge_sig", + &merge_sig), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &reserve_pub)); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if ( (no_reserve) && + ( (TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA + == rpc->flags) || + (! TALER_amount_is_zero (&rpc->gf->fees.purse)) ) ) + { + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (insufficient_funds) + { + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + /* 3) if present, persist contract */ + if (! rpc->no_econtract) + { + bool in_conflict = true; + + qs = TEH_plugin->insert_contract (TEH_plugin->cls, + &rpc->pd.purse_pub, + &rpc->econtract, + &in_conflict); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ("Failed to store purse information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse purse contract"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (in_conflict) + { + struct TALER_EncryptedContract econtract; + struct GNUNET_HashCode h_econtract; + + qs = TEH_plugin->select_contract_by_purse ( + TEH_plugin->cls, + &rpc->pd.purse_pub, + &econtract); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0 != qs); + TALER_LOG_WARNING ( + "Failed to store fetch contract information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select contract"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_CRYPTO_hash (econtract.econtract, + econtract.econtract_size, + &h_econtract); + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA), + GNUNET_JSON_pack_data_auto ("h_econtract", + &h_econtract), + GNUNET_JSON_pack_data_auto ("econtract_sig", + &econtract.econtract_sig), + GNUNET_JSON_pack_data_auto ("contract_pub", + &econtract.contract_pub)); + GNUNET_free (econtract.econtract); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_purse ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct MHD_Connection *connection = rc->connection; + struct ReservePurseContext rpc = { + .reserve_pub = reserve_pub, + .exchange_timestamp = GNUNET_TIME_timestamp_get () + }; + bool no_purse_fee = true; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("purse_value", + TEH_currency, + &rpc.pd.target_amount), + GNUNET_JSON_spec_uint32 ("min_age", + &rpc.min_age), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount ("purse_fee", + TEH_currency, + &rpc.purse_fee), + &no_purse_fee), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_econtract ("econtract", + &rpc.econtract), + &rpc.no_econtract), + GNUNET_JSON_spec_fixed_auto ("merge_pub", + &rpc.merge_pub), + GNUNET_JSON_spec_fixed_auto ("merge_sig", + &rpc.merge_sig), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rpc.reserve_sig), + GNUNET_JSON_spec_fixed_auto ("purse_pub", + &rpc.pd.purse_pub), + GNUNET_JSON_spec_fixed_auto ("purse_sig", + &rpc.purse_sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &rpc.pd.h_contract_terms), + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &rpc.merge_timestamp), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &rpc.pd.purse_expiration), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + { + char *payto_uri; + + payto_uri = TALER_reserve_make_payto (TEH_base_url, + reserve_pub); + TALER_payto_hash (payto_uri, + &rpc.h_payto); + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_purse_merge_verify (payto_uri, + rpc.merge_timestamp, + &rpc.pd.purse_pub, + &rpc.merge_pub, + &rpc.merge_sig)) + { + MHD_RESULT ret; + + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID, + payto_uri); + GNUNET_free (payto_uri); + return ret; + } + GNUNET_free (payto_uri); + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &rpc.deposit_total)); + if (GNUNET_TIME_timestamp_cmp (rpc.pd.purse_expiration, + <, + rpc.exchange_timestamp)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW, + NULL); + } + if (GNUNET_TIME_absolute_is_never (rpc.pd.purse_expiration.abs_time)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER, + NULL); + } + { + struct TEH_KeyStateHandle *keys; + + keys = TEH_keys_get_state (); + if (NULL == keys) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + rpc.gf = TEH_keys_global_fee_by_time (keys, + rpc.exchange_timestamp); + } + if (NULL == rpc.gf) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot purse purse: global fees not configured!\n"); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING, + NULL); + } + if (no_purse_fee) + { + rpc.flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &rpc.purse_fee)); + } + else + { + rpc.flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; + if (-1 == + TALER_amount_cmp (&rpc.purse_fee, + &rpc.gf->fees.purse)) + { + /* rpc.purse_fee is below gf.fees.purse! */ + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW, + TALER_amount2s (&rpc.gf->fees.purse)); + } + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_purse_create_verify (rpc.pd.purse_expiration, + &rpc.pd.h_contract_terms, + &rpc.merge_pub, + rpc.min_age, + &rpc.pd.target_amount, + &rpc.pd.purse_pub, + &rpc.purse_sig)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID, + NULL); + } + if (GNUNET_OK != + TALER_wallet_account_merge_verify (rpc.merge_timestamp, + &rpc.pd.purse_pub, + rpc.pd.purse_expiration, + &rpc.pd.h_contract_terms, + &rpc.pd.target_amount, + &rpc.purse_fee, + rpc.min_age, + rpc.flags, + rpc.reserve_pub, + &rpc.reserve_sig)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID, + NULL); + } + if ( (! rpc.no_econtract) && + (GNUNET_OK != + TALER_wallet_econtract_upload_verify (rpc.econtract.econtract, + rpc.econtract.econtract_size, + &rpc.econtract.contract_pub, + &rpc.pd.purse_pub, + &rpc.econtract.econtract_sig)) ) + { + TALER_LOG_WARNING ("Invalid signature on /reserves/$PID/purse request\n"); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID, + NULL); + } + + + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure"); + } + + /* execute transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "execute purse purse", + TEH_MT_REQUEST_RESERVE_PURSE, + &mhd_ret, + &purse_transaction, + &rpc)) + { + GNUNET_JSON_parse_free (spec); + return mhd_ret; + } + } + + if (! rpc.kyc.ok) + return TEH_RESPONSE_reply_kyc_required (connection, + &rpc.h_payto, + &rpc.kyc); + /* generate regular response */ + { + MHD_RESULT res; + + res = TEH_RESPONSE_reply_purse_created (connection, + rpc.exchange_timestamp, + &rpc.deposit_total, + &rpc.pd); + GNUNET_JSON_parse_free (spec); + return res; + } +} + + +/* end of taler-exchange-httpd_reserves_purse.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_purse.h b/src/exchange/taler-exchange-httpd_reserves_purse.h new file mode 100644 index 000000000..017e357d2 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_purse.h @@ -0,0 +1,46 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_purse.h + * @brief Handle /reserves/$RID/purse requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_RESERVES_PURSE_H +#define TALER_EXCHANGE_HTTPD_RESERVES_PURSE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/reserves/$RESERVE_PUB/purse" request. Parses the JSON, and, if + * successful, passes the JSON data to #create_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses create" being executed, or rejected. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_purse ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index 66a3b0af9..8993ea50f 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.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 Affero General Public License as published by the Free Software @@ -23,379 +23,21 @@ * @author Christian Grothoff */ #include "platform.h" +#include <gnunet/gnunet_json_lib.h> +#include <microhttpd.h> #include <zlib.h> #include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_plugin.h" #include "taler_util.h" #include "taler_json_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_keys.h" -/** - * Compile the transaction history of a coin into a JSON object. - * - * @param coin_pub public key of the coin - * @param tl transaction history to JSON-ify - * @return json representation of the @a rh, NULL on error - */ -json_t * -TEH_RESPONSE_compile_transaction_history ( - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_EXCHANGEDB_TransactionList *tl) -{ - json_t *history; - - history = json_array (); - if (NULL == history) - { - GNUNET_break (0); /* out of memory!? */ - return NULL; - } - for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; - NULL != pos; - pos = pos->next) - { - switch (pos->type) - { - case TALER_EXCHANGEDB_TT_DEPOSIT: - { - const struct TALER_EXCHANGEDB_DepositListEntry *deposit = - pos->details.deposit; - struct TALER_MerchantWireHash h_wire; - - TALER_merchant_wire_signature_hash (deposit->receiver_wire_account, - &deposit->wire_salt, - &h_wire); -#if ENABLE_SANITY_CHECKS - /* internal sanity check before we hand out a bogus sig... */ - if (GNUNET_OK != - TALER_wallet_deposit_verify (&deposit->amount_with_fee, - &deposit->deposit_fee, - &h_wire, - &deposit->h_contract_terms, - NULL /* h_extensions! */, - &deposit->h_denom_pub, - deposit->timestamp, - &deposit->merchant_pub, - deposit->refund_deadline, - coin_pub, - &deposit->csig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } -#endif - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "DEPOSIT"), - TALER_JSON_pack_amount ("amount", - &deposit->amount_with_fee), - TALER_JSON_pack_amount ("deposit_fee", - &deposit->deposit_fee), - GNUNET_JSON_pack_timestamp ("timestamp", - deposit->timestamp), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("refund_deadline", - deposit->refund_deadline)), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &deposit->merchant_pub), - GNUNET_JSON_pack_data_auto ("h_contract_terms", - &deposit->h_contract_terms), - GNUNET_JSON_pack_data_auto ("h_wire", - &h_wire), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &deposit->h_denom_pub), - GNUNET_JSON_pack_data_auto ("coin_sig", - &deposit->csig)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - case TALER_EXCHANGEDB_TT_MELT: - { - const struct TALER_EXCHANGEDB_MeltListEntry *melt = - pos->details.melt; - -#if ENABLE_SANITY_CHECKS - if (GNUNET_OK != - TALER_wallet_melt_verify (&melt->amount_with_fee, - &melt->melt_fee, - &melt->rc, - &melt->h_denom_pub, - coin_pub, - &melt->coin_sig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } -#endif - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "MELT"), - TALER_JSON_pack_amount ("amount", - &melt->amount_with_fee), - TALER_JSON_pack_amount ("melt_fee", - &melt->melt_fee), - GNUNET_JSON_pack_data_auto ("rc", - &melt->rc), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &melt->h_denom_pub), - GNUNET_JSON_pack_data_auto ("coin_sig", - &melt->coin_sig)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_TT_REFUND: - { - const struct TALER_EXCHANGEDB_RefundListEntry *refund = - pos->details.refund; - struct TALER_Amount value; - -#if ENABLE_SANITY_CHECKS - if (GNUNET_OK != - TALER_merchant_refund_verify (coin_pub, - &refund->h_contract_terms, - refund->rtransaction_id, - &refund->refund_amount, - &refund->merchant_pub, - &refund->merchant_sig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } -#endif - if (0 > - TALER_amount_subtract (&value, - &refund->refund_amount, - &refund->refund_fee)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "REFUND"), - TALER_JSON_pack_amount ("amount", - &value), - TALER_JSON_pack_amount ("refund_fee", - &refund->refund_fee), - GNUNET_JSON_pack_data_auto ("h_contract_terms", - &refund->h_contract_terms), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &refund->merchant_pub), - GNUNET_JSON_pack_uint64 ("rtransaction_id", - refund->rtransaction_id), - GNUNET_JSON_pack_data_auto ("merchant_sig", - &refund->merchant_sig)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: - { - struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = - pos->details.old_coin_recoup; - struct TALER_ExchangePublicKeyP epub; - struct TALER_ExchangeSignatureP esig; - struct TALER_RecoupRefreshConfirmationPS pc = { - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH), - .purpose.size = htonl (sizeof (pc)), - .timestamp = GNUNET_TIME_timestamp_hton (pr->timestamp), - .coin_pub = pr->coin.coin_pub, - .old_coin_pub = pr->old_coin_pub - }; - - TALER_amount_hton (&pc.recoup_amount, - &pr->value); - if (TALER_EC_NONE != - TEH_keys_exchange_sign (&pc, - &epub, - &esig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - /* NOTE: we could also provide coin_pub's coin_sig, denomination key hash and - the denomination key's RSA signature over coin_pub, but as the - wallet should really already have this information (and cannot - check or do anything with it anyway if it doesn't), it seems - strictly unnecessary. */ - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "OLD-COIN-RECOUP"), - TALER_JSON_pack_amount ("amount", - &pr->value), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &esig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &epub), - GNUNET_JSON_pack_data_auto ("coin_pub", - &pr->coin.coin_pub), - GNUNET_JSON_pack_timestamp ("timestamp", - pr->timestamp)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - case TALER_EXCHANGEDB_TT_RECOUP: - { - const struct TALER_EXCHANGEDB_RecoupListEntry *recoup = - pos->details.recoup; - struct TALER_ExchangePublicKeyP epub; - struct TALER_ExchangeSignatureP esig; - struct TALER_RecoupConfirmationPS pc = { - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP), - .purpose.size = htonl (sizeof (pc)), - .timestamp = GNUNET_TIME_timestamp_hton (recoup->timestamp), - .coin_pub = *coin_pub, - .reserve_pub = recoup->reserve_pub - }; - - TALER_amount_hton (&pc.recoup_amount, - &recoup->value); - if (TALER_EC_NONE != - TEH_keys_exchange_sign (&pc, - &epub, - &esig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RECOUP"), - TALER_JSON_pack_amount ("amount", - &recoup->value), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &esig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &epub), - GNUNET_JSON_pack_data_auto ("reserve_pub", - &recoup->reserve_pub), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &recoup->h_denom_pub), - GNUNET_JSON_pack_data_auto ("coin_sig", - &recoup->coin_sig), - GNUNET_JSON_pack_data_auto ("coin_blind", - &recoup->coin_blind), - GNUNET_JSON_pack_data_auto ("reserve_pub", - &recoup->reserve_pub), - GNUNET_JSON_pack_timestamp ("timestamp", - recoup->timestamp)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: - { - struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = - pos->details.recoup_refresh; - struct TALER_ExchangePublicKeyP epub; - struct TALER_ExchangeSignatureP esig; - struct TALER_RecoupRefreshConfirmationPS pc = { - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH), - .purpose.size = htonl (sizeof (pc)), - .timestamp = GNUNET_TIME_timestamp_hton (pr->timestamp), - .coin_pub = *coin_pub, - .old_coin_pub = pr->old_coin_pub - }; - - TALER_amount_hton (&pc.recoup_amount, - &pr->value); - if (TALER_EC_NONE != - TEH_keys_exchange_sign (&pc, - &epub, - &esig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - /* NOTE: we could also provide coin_pub's coin_sig, denomination key - hash and the denomination key's RSA signature over coin_pub, but as - the wallet should really already have this information (and cannot - check or do anything with it anyway if it doesn't), it seems - strictly unnecessary. */// - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RECOUP-REFRESH"), - TALER_JSON_pack_amount ("amount", - &pr->value), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &esig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &epub), - GNUNET_JSON_pack_data_auto ("old_coin_pub", - &pr->old_coin_pub), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &pr->coin.denom_pub_hash), - GNUNET_JSON_pack_data_auto ("coin_sig", - &pr->coin_sig), - GNUNET_JSON_pack_data_auto ("coin_blind", - &pr->coin_blind), - GNUNET_JSON_pack_timestamp ("timestamp", - pr->timestamp)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - default: - GNUNET_assert (0); - } - } - return history; -} - - MHD_RESULT TEH_RESPONSE_reply_unknown_denom_pub_hash ( struct MHD_Connection *connection, - const struct TALER_DenominationHash *dph) + const struct TALER_DenominationHashP *dph) { struct TALER_ExchangePublicKeyP epub; struct TALER_ExchangeSignatureP esig; @@ -403,18 +45,12 @@ TEH_RESPONSE_reply_unknown_denom_pub_hash ( enum TALER_ErrorCode ec; now = GNUNET_TIME_timestamp_get (); - { - struct TALER_DenominationUnknownAffirmationPS dua = { - .purpose.size = htonl (sizeof (dua)), - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_UNKNOWN), - .timestamp = GNUNET_TIME_timestamp_hton (now), - .h_denom_pub = *dph, - }; - - ec = TEH_keys_exchange_sign (&dua, - &epub, - &esig); - } + ec = TALER_exchange_online_denomination_unknown_sign ( + &TEH_keys_exchange_sign_, + now, + dph, + &epub, + &esig); if (TALER_EC_NONE != ec) { GNUNET_break (0); @@ -441,7 +77,7 @@ TEH_RESPONSE_reply_unknown_denom_pub_hash ( MHD_RESULT TEH_RESPONSE_reply_expired_denom_pub_hash ( struct MHD_Connection *connection, - const struct TALER_DenominationHash *dph, + const struct TALER_DenominationHashP *dph, enum TALER_ErrorCode ec, const char *oper) { @@ -450,22 +86,14 @@ TEH_RESPONSE_reply_expired_denom_pub_hash ( enum TALER_ErrorCode ecr; struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); - struct TALER_DenominationExpiredAffirmationPS dua = { - .purpose.size = htonl (sizeof (dua)), - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED), - .timestamp = GNUNET_TIME_timestamp_hton (now), - .h_denom_pub = *dph, - }; - - /* strncpy would create a compiler warning */ - memcpy (dua.operation, - oper, - GNUNET_MIN (sizeof (dua.operation), - strlen (oper))); - ecr = TEH_keys_exchange_sign (&dua, - &epub, - &esig); + + ecr = TALER_exchange_online_denomination_expired_sign ( + &TEH_keys_exchange_sign_, + now, + dph, + oper, + &epub, + &esig); if (TALER_EC_NONE != ecr) { GNUNET_break (0); @@ -491,356 +119,290 @@ TEH_RESPONSE_reply_expired_denom_pub_hash ( } -/** - * Send proof that a request is invalid to client because of - * insufficient funds. This function will create a message with all - * of the operations affecting the coin that demonstrate that the coin - * has insufficient value. - * - * @param connection connection to the client - * @param ec error code to return - * @param coin_pub public key of the coin - * @param tl transaction list to use to build reply - * @return MHD result code - */ MHD_RESULT -TEH_RESPONSE_reply_coin_insufficient_funds ( +TEH_RESPONSE_reply_invalid_denom_cipher_for_operation ( struct MHD_Connection *connection, - enum TALER_ErrorCode ec, - const struct TALER_CoinSpendPublicKeyP *coin_pub) + const struct TALER_DenominationHashP *dph) { - struct TALER_EXCHANGEDB_TransactionList *tl; - enum GNUNET_DB_QueryStatus qs; - json_t *history; - - // FIXME: maybe start read-committed transaction here? - // => check all callers (that they aborted already!) - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - coin_pub, - GNUNET_NO, - &tl); - if (0 > qs) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + struct GNUNET_TIME_Timestamp now; + enum TALER_ErrorCode ec; - history = TEH_RESPONSE_compile_transaction_history (coin_pub, - tl); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - if (NULL == history) + now = GNUNET_TIME_timestamp_get (); + ec = TALER_exchange_online_denomination_unknown_sign ( + &TEH_keys_exchange_sign_, + now, + dph, + &epub, + &esig); + if (TALER_EC_NONE != ec) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - "Failed to generated proof of insufficient funds"); + ec, + NULL); } return TALER_MHD_REPLY_JSON_PACK ( connection, + MHD_HTTP_NOT_FOUND, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION), + GNUNET_JSON_pack_timestamp ("timestamp", + now), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + dph)); +} + + +MHD_RESULT +TEH_RESPONSE_reply_coin_insufficient_funds ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + const struct TALER_DenominationHashP *h_denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, TALER_ErrorCode_get_http_status_safe (ec), TALER_JSON_pack_ec (ec), - GNUNET_JSON_pack_array_steal ("history", - history)); + GNUNET_JSON_pack_data_auto ("coin_pub", + coin_pub), + // FIXME - #7267: to be kept only for some of the error types! + GNUNET_JSON_pack_data_auto ("h_denom_pub", + h_denom_pub)); } -/** - * Compile the history of a reserve into a JSON object - * and calculate the total balance. - * - * @param rh reserve history to JSON-ify - * @param[out] balance set to current reserve balance - * @return json representation of the @a rh, NULL on error - */ -json_t * -TEH_RESPONSE_compile_reserve_history ( - const struct TALER_EXCHANGEDB_ReserveHistory *rh, - struct TALER_Amount *balance) +MHD_RESULT +TEH_RESPONSE_reply_coin_conflicting_contract ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + const struct TALER_MerchantWireHashP *h_wire) { - struct TALER_Amount credit_total; - struct TALER_Amount withdraw_total; - json_t *json_history; - enum InitAmounts - { - /** Nothing initialized */ - IA_NONE = 0, - /** credit_total initialized */ - IA_CREDIT = 1, - /** withdraw_total initialized */ - IA_WITHDRAW = 2 - } init = IA_NONE; - - json_history = json_array (); - for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh; - NULL != pos; - pos = pos->next) + return TALER_MHD_REPLY_JSON_PACK ( + connection, + TALER_ErrorCode_get_http_status_safe (ec), + GNUNET_JSON_pack_data_auto ("h_wire", + h_wire), + TALER_JSON_pack_ec (ec)); +} + + +MHD_RESULT +TEH_RESPONSE_reply_coin_denomination_conflict ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationPublicKey *prev_denom_pub, + const struct TALER_DenominationSignature *prev_denom_sig) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, + TALER_ErrorCode_get_http_status_safe (ec), + TALER_JSON_pack_ec (ec), + GNUNET_JSON_pack_data_auto ("coin_pub", + coin_pub), + TALER_JSON_pack_denom_pub ("prev_denom_pub", + prev_denom_pub), + TALER_JSON_pack_denom_sig ("prev_denom_sig", + prev_denom_sig) + ); + +} + + +MHD_RESULT +TEH_RESPONSE_reply_coin_age_commitment_conflict ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + enum TALER_EXCHANGEDB_CoinKnownStatus status, + const struct TALER_DenominationHashP *h_denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_AgeCommitmentHash *h_age_commitment) +{ + const char *conflict_detail; + + switch (status) { - switch (pos->type) - { - case TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE: - { - const struct TALER_EXCHANGEDB_BankTransfer *bank = - pos->details.bank; - if (0 == (IA_CREDIT & init)) - { - credit_total = bank->amount; - init |= IA_CREDIT; - } - else if (0 > - TALER_amount_add (&credit_total, - &credit_total, - &bank->amount)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "CREDIT"), - GNUNET_JSON_pack_timestamp ("timestamp", - bank->execution_date), - GNUNET_JSON_pack_string ("sender_account_url", - bank->sender_account_details), - GNUNET_JSON_pack_uint64 ("wire_reference", - bank->wire_reference), - TALER_JSON_pack_amount ("amount", - &bank->amount)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - break; - } - case TALER_EXCHANGEDB_RO_WITHDRAW_COIN: - { - const struct TALER_EXCHANGEDB_CollectableBlindcoin *withdraw - = pos->details.withdraw; - struct TALER_Amount value; - - value = withdraw->amount_with_fee; - if (0 == (IA_WITHDRAW & init)) - { - withdraw_total = value; - init |= IA_WITHDRAW; - } - else - { - if (0 > - TALER_amount_add (&withdraw_total, - &withdraw_total, - &value)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "WITHDRAW"), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &withdraw->reserve_sig), - GNUNET_JSON_pack_data_auto ("h_coin_envelope", - &withdraw->h_coin_envelope), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &withdraw->denom_pub_hash), - TALER_JSON_pack_amount ("withdraw_fee", - &withdraw->withdraw_fee), - TALER_JSON_pack_amount ("amount", - &value)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_RO_RECOUP_COIN: - { - const struct TALER_EXCHANGEDB_Recoup *recoup - = pos->details.recoup; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - - if (0 == (IA_CREDIT & init)) - { - credit_total = recoup->value; - init |= IA_CREDIT; - } - else if (0 > - TALER_amount_add (&credit_total, - &credit_total, - &recoup->value)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - { - struct TALER_RecoupConfirmationPS pc = { - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP), - .purpose.size = htonl (sizeof (pc)), - .timestamp = GNUNET_TIME_timestamp_hton (recoup->timestamp), - .coin_pub = recoup->coin.coin_pub, - .reserve_pub = recoup->reserve_pub - }; - - TALER_amount_hton (&pc.recoup_amount, - &recoup->value); - if (TALER_EC_NONE != - TEH_keys_exchange_sign (&pc, - &pub, - &sig)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RECOUP"), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_timestamp ("timestamp", - recoup->timestamp), - TALER_JSON_pack_amount ("amount", - &recoup->value), - GNUNET_JSON_pack_data_auto ("coin_pub", - &recoup->coin.coin_pub)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_RO_EXCHANGE_TO_BANK: - { - const struct TALER_EXCHANGEDB_ClosingTransfer *closing = - pos->details.closing; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - struct TALER_Amount value; - - value = closing->amount; - if (0 == (IA_WITHDRAW & init)) - { - withdraw_total = value; - init |= IA_WITHDRAW; - } - else - { - if (0 > - TALER_amount_add (&withdraw_total, - &withdraw_total, - &value)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - { - struct TALER_ReserveCloseConfirmationPS rcc = { - .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED), - .purpose.size = htonl (sizeof (rcc)), - .timestamp = GNUNET_TIME_timestamp_hton (closing->execution_date), - .reserve_pub = pos->details.closing->reserve_pub, - .wtid = closing->wtid - }; - - TALER_amount_hton (&rcc.closing_amount, - &value); - TALER_amount_hton (&rcc.closing_fee, - &closing->closing_fee); - TALER_payto_hash (closing->receiver_account_details, - &rcc.h_payto); - if (TALER_EC_NONE != - TEH_keys_exchange_sign (&rcc, - &pub, - &sig)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "CLOSING"), - GNUNET_JSON_pack_string ("receiver_account_details", - closing->receiver_account_details), - GNUNET_JSON_pack_data_auto ("wtid", - &closing->wtid), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_timestamp ("timestamp", - closing->execution_date), - TALER_JSON_pack_amount ("amount", - &value), - TALER_JSON_pack_amount ("closing_fee", - &closing->closing_fee)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - } + + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL: + conflict_detail = "expected NULL age commitment hash"; + h_age_commitment = NULL; + break; + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL: + conflict_detail = "expected non-NULL age commitment hash"; + break; + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS: + conflict_detail = "expected age commitment hash differs"; + break; + default: + GNUNET_assert (0); } - if (0 == (IA_CREDIT & init)) + return TALER_MHD_REPLY_JSON_PACK ( + connection, + TALER_ErrorCode_get_http_status_safe (ec), + TALER_JSON_pack_ec (ec), + GNUNET_JSON_pack_data_auto ("coin_pub", + coin_pub), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + h_denom_pub), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("expected_age_commitment_hash", + h_age_commitment)), + GNUNET_JSON_pack_string ("conflict_detail", + conflict_detail) + ); +} + + +MHD_RESULT +TEH_RESPONSE_reply_reserve_insufficient_balance ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + const struct TALER_Amount *reserve_balance, + const struct TALER_Amount *balance_required, + const struct TALER_ReservePublicKeyP *reserve_pub) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec (ec), + TALER_JSON_pack_amount ("balance", + reserve_balance), + TALER_JSON_pack_amount ("requested_amount", + balance_required)); +} + + +MHD_RESULT +TEH_RESPONSE_reply_reserve_age_restriction_required ( + struct MHD_Connection *connection, + uint16_t maximum_allowed_age) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec (TALER_EC_EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED), + GNUNET_JSON_pack_uint64 ("maximum_allowed_age", + maximum_allowed_age)); +} + + +MHD_RESULT +TEH_RESPONSE_reply_purse_created ( + struct MHD_Connection *connection, + struct GNUNET_TIME_Timestamp exchange_timestamp, + const struct TALER_Amount *purse_balance, + const struct TEH_PurseDetails *pd) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_purse_created_sign ( + &TEH_keys_exchange_sign_, + exchange_timestamp, + pd->purse_expiration, + &pd->target_amount, + purse_balance, + &pd->purse_pub, + &pd->h_contract_terms, + &pub, + &sig))) { - /* We should not have gotten here, without credits no reserve - should exist! */ GNUNET_break (0); - json_decref (json_history); - return NULL; + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); } - if (0 == (IA_WITHDRAW & init)) - { - /* did not encounter any withdraw operations, set withdraw_total to zero */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (credit_total.currency, - &withdraw_total)); - } - if (0 > - TALER_amount_subtract (balance, - &credit_total, - &withdraw_total)) + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("total_deposited", + purse_balance), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + exchange_timestamp), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +MHD_RESULT +TEH_RESPONSE_reply_kyc_required (struct MHD_Connection *connection, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_EXCHANGEDB_KycStatus *kyc) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + TALER_JSON_pack_ec (TALER_EC_EXCHANGE_GENERIC_KYC_REQUIRED), + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + GNUNET_JSON_pack_uint64 ("requirement_row", + kyc->requirement_row)); +} + + +MHD_RESULT +TEH_RESPONSE_reply_aml_blocked (struct MHD_Connection *connection, + enum TALER_AmlDecisionState status) +{ + enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + + switch (status) { + case TALER_AML_NORMAL: GNUNET_break (0); - json_decref (json_history); - return NULL; + return MHD_NO; + case TALER_AML_PENDING: + ec = TALER_EC_EXCHANGE_GENERIC_AML_PENDING; + break; + case TALER_AML_FROZEN: + ec = TALER_EC_EXCHANGE_GENERIC_AML_FROZEN; + break; } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + TALER_JSON_pack_ec (ec)); +} - return json_history; + +MHD_RESULT +TEH_RESPONSE_reply_not_modified ( + struct MHD_Connection *connection, + const char *etags, + TEH_RESPONSE_SetHeaders cb, + void *cb_cls) +{ + MHD_RESULT ret; + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + cb (cb_cls, + resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etags)); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; } diff --git a/src/exchange/taler-exchange-httpd_responses.h b/src/exchange/taler-exchange-httpd_responses.h index db2286ffa..24b24621f 100644 --- a/src/exchange/taler-exchange-httpd_responses.h +++ b/src/exchange/taler-exchange-httpd_responses.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2019 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 Affero General Public License as published by the Free Software @@ -24,41 +24,90 @@ */ #ifndef TALER_EXCHANGE_HTTPD_RESPONSES_H #define TALER_EXCHANGE_HTTPD_RESPONSES_H + #include <gnunet/gnunet_util_lib.h> #include <jansson.h> #include <microhttpd.h> #include "taler_error_codes.h" #include "taler-exchange-httpd.h" #include "taler-exchange-httpd_db.h" -#include <gnunet/gnunet_mhd_compat.h> +#include "taler_exchangedb_plugin.h" /** - * Compile the history of a reserve into a JSON object - * and calculate the total balance. + * Send assertion that the given denomination key hash + * is unknown to us at this time. * - * @param rh reserve history to JSON-ify - * @param[out] balance set to current reserve balance - * @return json representation of the @a rh, NULL on error + * @param connection connection to the client + * @param dph denomination public key hash + * @return MHD result code */ -json_t * -TEH_RESPONSE_compile_reserve_history ( - const struct TALER_EXCHANGEDB_ReserveHistory *rh, - struct TALER_Amount *balance); +MHD_RESULT +TEH_RESPONSE_reply_unknown_denom_pub_hash ( + struct MHD_Connection *connection, + const struct TALER_DenominationHashP *dph); /** - * Send assertion that the given denomination key hash - * is unknown to us at this time. + * Return error message indicating that a reserve had + * an insufficient balance for the given operation. * * @param connection connection to the client - * @param dph denomination public key hash + * @param ec specific error code to return with the reserve history + * @param reserve_balance balance remaining in the reserve + * @param balance_required the balance required for the operation + * @param reserve_pub the reserve with insufficient balance * @return MHD result code */ MHD_RESULT -TEH_RESPONSE_reply_unknown_denom_pub_hash ( +TEH_RESPONSE_reply_reserve_insufficient_balance ( struct MHD_Connection *connection, - const struct TALER_DenominationHash *dph); + enum TALER_ErrorCode ec, + const struct TALER_Amount *reserve_balance, + const struct TALER_Amount *balance_required, + const struct TALER_ReservePublicKeyP *reserve_pub); + +/** + * Return error message indicating that a reserve requires age + * restriction to be set during withdraw, that is: the age-withdraw + * protocol MUST be used with commitment to an admissible age. + * + * @param connection connection to the client + * @param maximum_allowed_age the balance required for the operation + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_reserve_age_restriction_required ( + struct MHD_Connection *connection, + uint16_t maximum_allowed_age); + + +/** + * Send information that a KYC check must be + * satisfied to proceed to client. + * + * @param connection connection to the client + * @param h_payto account identifier to include in reply + * @param kyc details about the KYC requirements + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_kyc_required (struct MHD_Connection *connection, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_EXCHANGEDB_KycStatus *kyc); + + +/** + * Send information that an AML process is blocking + * the operation right now. + * + * @param connection connection to the client + * @param status current AML status + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_aml_blocked (struct MHD_Connection *connection, + enum TALER_AmlDecisionState status); /** @@ -74,12 +123,25 @@ TEH_RESPONSE_reply_unknown_denom_pub_hash ( MHD_RESULT TEH_RESPONSE_reply_expired_denom_pub_hash ( struct MHD_Connection *connection, - const struct TALER_DenominationHash *dph, + const struct TALER_DenominationHashP *dph, enum TALER_ErrorCode ec, const char *oper); /** + * Send assertion that the given denomination cannot be used for this operation. + * + * @param connection connection to the client + * @param dph denomination public key hash + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_invalid_denom_cipher_for_operation ( + struct MHD_Connection *connection, + const struct TALER_DenominationHashP *dph); + + +/** * Send proof that a request is invalid to client because of * insufficient funds. This function will create a message with all * of the operations affecting the coin that demonstrate that the coin @@ -87,6 +149,7 @@ TEH_RESPONSE_reply_expired_denom_pub_hash ( * * @param connection connection to the client * @param ec error code to return + * @param h_denom_pub hash of the denomination of the coin * @param coin_pub public key of the coin * @return MHD result code */ @@ -94,20 +157,143 @@ MHD_RESULT TEH_RESPONSE_reply_coin_insufficient_funds ( struct MHD_Connection *connection, enum TALER_ErrorCode ec, + const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_CoinSpendPublicKeyP *coin_pub); +/** + * Send proof that a request is invalid to client because of + * an conflict with the provided denomination (the exchange had seen + * this coin before, signed by a different denomination). + * This function will create a message with the denomination's public key + * that was seen before. + * + * @param connection connection to the client + * @param ec error code to return + * @param coin_pub the public key of the coin + * @param prev_denom_pub the denomination of the coin, as seen previously + * @param prev_denom_sig the signature with the denomination key over the coin + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_coin_denomination_conflict ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationPublicKey *prev_denom_pub, + const struct TALER_DenominationSignature *prev_denom_sig); + +/** + * Send the salted hash of the merchant's bank account from conflicting + * contract. With this information, the merchant's private key and + * the hash of the contract terms, the client can retrieve more details + * about the conflicting deposit + * + * @param connection connection to the client + * @param ec error code to return + * @param h_wire the salted hash of the merchant's bank account + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_coin_conflicting_contract ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + const struct TALER_MerchantWireHashP *h_wire); /** - * Compile the transaction history of a coin into a JSON object. + * Send proof that a request is invalid to client because of + * a conflicting value for the age commitment hash of a coin. + * This function will create a message with the conflicting + * hash value for the age commitment of the given coin. * + * @param connection connection to the client + * @param ec error code to return + * @param cks specific conflict type + * @param h_denom_pub hash of the denomination of the coin * @param coin_pub public key of the coin - * @param tl transaction history to JSON-ify - * @return json representation of the @a rh, NULL on error + * @param h_age_commitment hash of the age commitment as found in the database + * @return MHD result code */ -json_t * -TEH_RESPONSE_compile_transaction_history ( +MHD_RESULT +TEH_RESPONSE_reply_coin_age_commitment_conflict ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + enum TALER_EXCHANGEDB_CoinKnownStatus cks, + const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_EXCHANGEDB_TransactionList *tl); + const struct TALER_AgeCommitmentHash *h_age_commitment); + +/** + * Fundamental details about a purse. + */ +struct TEH_PurseDetails +{ + /** + * When should the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * Hash of the contract terms of the purse. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Public key of the purse we are creating. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * Total amount to be put into the purse. + */ + struct TALER_Amount target_amount; +}; + + +/** + * Send confirmation that a purse was created with + * the current purse balance. + * + * @param connection connection to the client + * @param pd purse details + * @param exchange_timestamp our time for purse creation + * @param purse_balance current balance in the purse + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_purse_created ( + struct MHD_Connection *connection, + struct GNUNET_TIME_Timestamp exchange_timestamp, + const struct TALER_Amount *purse_balance, + const struct TEH_PurseDetails *pd); + + +/** + * Callback used to set headers in a response. + * + * @param cls closure + * @param[in,out] resp response to modify + */ +typedef void +(*TEH_RESPONSE_SetHeaders)(void *cls, + struct MHD_Response *resp); + + +/** + * Generate a HTTP "Not modified" response with the + * given @a etags. + * + * @param connection connection to queue response on + * @param etags ETag header to set + * @param cb callback to modify response headers + * @param cb_cls closure for @a cb + * @return MHD result code + */ +MHD_RESULT +TEH_RESPONSE_reply_not_modified ( + struct MHD_Connection *connection, + const char *etags, + TEH_RESPONSE_SetHeaders cb, + void *cb_cls); #endif diff --git a/src/exchange/taler-exchange-httpd_spa.c b/src/exchange/taler-exchange-httpd_spa.c new file mode 100644 index 000000000..60bed3d28 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_spa.c @@ -0,0 +1,362 @@ +/* + 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 EXCHANGEABILITY 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 taler-exchange-httpd_spa.c + * @brief logic to load the single page app (/) + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "taler_util.h" +#include "taler_mhd_lib.h" +#include <gnunet/gnunet_mhd_compat.h> +#include "taler-exchange-httpd.h" + + +/** + * Resource from the WebUi. + */ +struct WebuiFile +{ + /** + * Kept in a DLL. + */ + struct WebuiFile *next; + + /** + * Kept in a DLL. + */ + struct WebuiFile *prev; + + /** + * Path this resource matches. + */ + char *path; + + /** + * SPA resource, compressed. + */ + struct MHD_Response *zspa; + + /** + * SPA resource, vanilla. + */ + struct MHD_Response *spa; + +}; + + +/** + * Resources of the WebuUI, kept in a DLL. + */ +static struct WebuiFile *webui_head; + +/** + * Resources of the WebuUI, kept in a DLL. + */ +static struct WebuiFile *webui_tail; + + +MHD_RESULT +TEH_handler_spa (struct TEH_RequestContext *rc, + const char *const args[]) +{ + struct WebuiFile *w = NULL; + const char *infix = args[0]; + + if ( (NULL == infix) || + (0 == strcmp (infix, + "")) ) + infix = "index.html"; + for (struct WebuiFile *pos = webui_head; + NULL != pos; + pos = pos->next) + if (0 == strcmp (infix, + pos->path)) + { + w = pos; + break; + } + if (NULL == w) + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + if ( (MHD_YES == + TALER_MHD_can_compress (rc->connection)) && + (NULL != w->zspa) ) + return MHD_queue_response (rc->connection, + MHD_HTTP_OK, + w->zspa); + return MHD_queue_response (rc->connection, + MHD_HTTP_OK, + w->spa); +} + + +/** + * Function called on each file to load for the WebUI. + * + * @param cls NULL + * @param dn name of the file to load + */ +static enum GNUNET_GenericReturnValue +build_webui (void *cls, + const char *dn) +{ + static struct + { + const char *ext; + const char *mime; + } mime_map[] = { + { + .ext = "css", + .mime = "text/css" + }, + { + .ext = "html", + .mime = "text/html" + }, + { + .ext = "js", + .mime = "text/javascript" + }, + { + .ext = "jpg", + .mime = "image/jpeg" + }, + { + .ext = "jpeg", + .mime = "image/jpeg" + }, + { + .ext = "png", + .mime = "image/png" + }, + { + .ext = "svg", + .mime = "image/svg+xml" + }, + { + .ext = NULL, + .mime = NULL + }, + }; + int fd; + struct stat sb; + struct MHD_Response *zspa = NULL; + struct MHD_Response *spa; + const char *ext; + const char *mime; + + (void) cls; + /* finally open template */ + fd = open (dn, + O_RDONLY); + if (-1 == fd) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + dn); + return GNUNET_SYSERR; + } + if (0 != + fstat (fd, + &sb)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + dn); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + + mime = NULL; + ext = strrchr (dn, '.'); + if (NULL == ext) + { + GNUNET_break (0 == close (fd)); + return GNUNET_OK; + } + ext++; + for (unsigned int i = 0; NULL != mime_map[i].ext; i++) + if (0 == strcasecmp (ext, + mime_map[i].ext)) + { + mime = mime_map[i].mime; + break; + } + + { + void *in; + ssize_t r; + size_t csize; + + in = GNUNET_malloc_large (sb.st_size); + if (NULL == in) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "malloc"); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + r = read (fd, + in, + sb.st_size); + if ( (-1 == r) || + (sb.st_size != (size_t) r) ) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "read", + dn); + GNUNET_free (in); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + csize = (size_t) r; + if (MHD_YES == + TALER_MHD_body_compress (&in, + &csize)) + { + zspa = MHD_create_response_from_buffer (csize, + in, + MHD_RESPMEM_MUST_FREE); + if (NULL != zspa) + { + if (MHD_NO == + MHD_add_response_header (zspa, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) + { + GNUNET_break (0); + MHD_destroy_response (zspa); + zspa = NULL; + } + if (NULL != mime) + GNUNET_break (MHD_YES == + MHD_add_response_header (zspa, + MHD_HTTP_HEADER_CONTENT_TYPE, + mime)); + } + } + else + { + GNUNET_free (in); + } + } + + spa = MHD_create_response_from_fd (sb.st_size, + fd); + if (NULL == spa) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + dn); + GNUNET_break (0 == close (fd)); + if (NULL != zspa) + { + MHD_destroy_response (zspa); + zspa = NULL; + } + return GNUNET_SYSERR; + } + if (NULL != mime) + GNUNET_break (MHD_YES == + MHD_add_response_header (spa, + MHD_HTTP_HEADER_CONTENT_TYPE, + mime)); + + { + struct WebuiFile *w; + const char *fn; + + fn = strrchr (dn, '/'); + GNUNET_assert (NULL != fn); + w = GNUNET_new (struct WebuiFile); + w->path = GNUNET_strdup (fn + 1); + w->spa = spa; + w->zspa = zspa; + GNUNET_CONTAINER_DLL_insert (webui_head, + webui_tail, + w); + } + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TEH_spa_init () +{ + char *dn; + + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_asprintf (&dn, + "%sexchange/spa/", + path); + GNUNET_free (path); + } + + if (-1 == + GNUNET_DISK_directory_scan (dn, + &build_webui, + NULL)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load WebUI from `%s'\n", + dn); + GNUNET_free (dn); + return GNUNET_SYSERR; + } + GNUNET_free (dn); + return GNUNET_OK; +} + + +/** + * Nicely shut down. + */ +void __attribute__ ((destructor)) +get_spa_fini () +{ + struct WebuiFile *w; + + while (NULL != (w = webui_head)) + { + GNUNET_CONTAINER_DLL_remove (webui_head, + webui_tail, + w); + if (NULL != w->spa) + { + MHD_destroy_response (w->spa); + w->spa = NULL; + } + if (NULL != w->zspa) + { + MHD_destroy_response (w->zspa); + w->zspa = NULL; + } + GNUNET_free (w->path); + GNUNET_free (w); + } +} diff --git a/src/exchange/taler-exchange-httpd_spa.h b/src/exchange/taler-exchange-httpd_spa.h new file mode 100644 index 000000000..4147a853b --- /dev/null +++ b/src/exchange/taler-exchange-httpd_spa.h @@ -0,0 +1,49 @@ +/* + 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 EXCHANGEABILITY 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 taler-exchange-httpd_spa.h + * @brief logic to preload and serve static files + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_SPA_H +#define TALER_EXCHANGE_HTTPD_SPA_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Return our single-page-app user interface (see contrib/wallet-core/). + * + * @param rc context of the handler + * @param[in,out] args remaining arguments (ignored) + * @return #MHD_YES on success (reply queued), #MHD_NO on error (close connection) + */ +MHD_RESULT +TEH_handler_spa (struct TEH_RequestContext *rc, + const char *const args[]); + + +/** + * Preload and compress SPA files. + * + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TEH_spa_init (void); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_transfers_get.c b/src/exchange/taler-exchange-httpd_transfers_get.c index 7ca0da974..18d96f955 100644 --- a/src/exchange/taler-exchange-httpd_transfers_get.c +++ b/src/exchange/taler-exchange-httpd_transfers_get.c @@ -51,7 +51,7 @@ struct AggregatedDepositDetail /** * Hash of the contract terms. */ - struct TALER_PrivateContractHash h_contract_terms; + struct TALER_PrivateContractHashP h_contract_terms; /** * Coin's public key of the deposited coin. @@ -59,14 +59,20 @@ struct AggregatedDepositDetail struct TALER_CoinSpendPublicKeyP coin_pub; /** - * Total value of the coin in the deposit. + * Total value of the coin in the deposit (after + * refunds). */ struct TALER_Amount deposit_value; /** - * Fees charged by the exchange for the deposit of this coin. + * Fees charged by the exchange for the deposit of this coin (possibly after reduction due to refunds). */ struct TALER_Amount deposit_fee; + + /** + * Total amount refunded for this coin. + */ + struct TALER_Amount refund_total; }; @@ -93,11 +99,11 @@ reply_transfer_details (struct MHD_Connection *connection, const struct AggregatedDepositDetail *wdd_head) { json_t *deposits; - struct TALER_WireDepositDetailP dd; struct GNUNET_HashContext *hash_context; - struct TALER_WireDepositDataPS wdp; + struct GNUNET_HashCode h_details; struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; + struct TALER_PaytoHashP h_payto; deposits = json_array (); GNUNET_assert (NULL != deposits); @@ -106,16 +112,12 @@ reply_transfer_details (struct MHD_Connection *connection, NULL != wdd_pos; wdd_pos = wdd_pos->next) { - dd.h_contract_terms = wdd_pos->h_contract_terms; - dd.execution_time = GNUNET_TIME_timestamp_hton (exec_time); - dd.coin_pub = wdd_pos->coin_pub; - TALER_amount_hton (&dd.deposit_value, - &wdd_pos->deposit_value); - TALER_amount_hton (&dd.deposit_fee, - &wdd_pos->deposit_fee); - GNUNET_CRYPTO_hash_context_read (hash_context, - &dd, - sizeof (struct TALER_WireDepositDetailP)); + TALER_exchange_online_wire_deposit_append (hash_context, + &wdd_pos->h_contract_terms, + exec_time, + &wdd_pos->coin_pub, + &wdd_pos->deposit_value, + &wdd_pos->deposit_fee); if (0 != json_array_append_new ( deposits, @@ -124,6 +126,13 @@ reply_transfer_details (struct MHD_Connection *connection, &wdd_pos->h_contract_terms), GNUNET_JSON_pack_data_auto ("coin_pub", &wdd_pos->coin_pub), + + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("refund_total", + TALER_amount_is_zero ( + &wdd_pos->refund_total) + ? NULL + : &wdd_pos->refund_total)), TALER_JSON_pack_amount ("deposit_value", &wdd_pos->deposit_value), TALER_JSON_pack_amount ("deposit_fee", @@ -137,24 +146,21 @@ reply_transfer_details (struct MHD_Connection *connection, "json_array_append_new() failed"); } } - wdp.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT); - wdp.purpose.size = htonl (sizeof (struct TALER_WireDepositDataPS)); - TALER_amount_hton (&wdp.total, - total); - TALER_amount_hton (&wdp.wire_fee, - wire_fee); - wdp.merchant_pub = *merchant_pub; - TALER_payto_hash (payto_uri, - &wdp.h_payto); GNUNET_CRYPTO_hash_context_finish (hash_context, - &wdp.h_details); + &h_details); { enum TALER_ErrorCode ec; if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign (&wdp, - &pub, - &sig))) + (ec = TALER_exchange_online_wire_deposit_sign ( + &TEH_keys_exchange_sign_, + total, + wire_fee, + merchant_pub, + payto_uri, + &h_details, + &pub, + &sig))) { json_decref (deposits); return TALER_MHD_reply_with_ec (connection, @@ -163,6 +169,8 @@ reply_transfer_details (struct MHD_Connection *connection, } } + TALER_payto_hash (payto_uri, + &h_payto); return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, @@ -173,7 +181,7 @@ reply_transfer_details (struct MHD_Connection *connection, GNUNET_JSON_pack_data_auto ("merchant_pub", merchant_pub), GNUNET_JSON_pack_data_auto ("h_payto", - &wdp.h_payto), + &h_payto), GNUNET_JSON_pack_timestamp ("execution_time", exec_time), GNUNET_JSON_pack_array_steal ("deposits", @@ -211,9 +219,9 @@ struct WtidTransactionContext struct TALER_MerchantPublicKeyP merchant_pub; /** - * Wire fee applicable at @e exec_time. + * Wire fees applicable at @e exec_time. */ - struct TALER_Amount wire_fee; + struct TALER_WireFeeSet fees; /** * Execution time of the wire transfer @@ -253,6 +261,31 @@ struct WtidTransactionContext /** + * Callback that totals up the applicable refunds. + * + * @param cls a `struct TALER_Amount` where we keep the total + * @param amount_with_fee amount being refunded + */ +static enum GNUNET_GenericReturnValue +add_refunds (void *cls, + const struct TALER_Amount *amount_with_fee) + +{ + struct TALER_Amount *total = cls; + + if (0 > + TALER_amount_add (total, + total, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** * Function called with the results of the lookup of the individual deposits * that were aggregated for the given wire transfer. * @@ -260,31 +293,101 @@ struct WtidTransactionContext * @param rowid which row in the DB is the information from (for diagnostics), ignored * @param merchant_pub public key of the merchant (should be same for all callbacks with the same @e cls) * @param account_payto_uri where the funds were sent + * @param h_payto hash over @a account_payto_uri as it is in the DB * @param exec_time execution time of the wire transfer (should be same for all callbacks with the same @e cls) * @param h_contract_terms which proposal was this payment about * @param denom_pub denomination public key of the @a coin_pub (ignored) * @param coin_pub which public key was this payment about - * @param deposit_value amount contributed by this coin in total + * @param deposit_value amount contributed by this coin in total (including fee) * @param deposit_fee deposit fee charged by exchange for this coin */ static void -handle_deposit_data (void *cls, - uint64_t rowid, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const char *account_payto_uri, - struct GNUNET_TIME_Timestamp exec_time, - const struct TALER_PrivateContractHash *h_contract_terms, - const struct TALER_DenominationPublicKey *denom_pub, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *deposit_value, - const struct TALER_Amount *deposit_fee) +handle_deposit_data ( + void *cls, + uint64_t rowid, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const char *account_payto_uri, + const struct TALER_PaytoHashP *h_payto, + struct GNUNET_TIME_Timestamp exec_time, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *deposit_value, + const struct TALER_Amount *deposit_fee) { struct WtidTransactionContext *ctx = cls; + struct TALER_Amount total_refunds; + struct TALER_Amount dval; + struct TALER_Amount dfee; + enum GNUNET_DB_QueryStatus qs; (void) rowid; (void) denom_pub; + (void) h_payto; if (GNUNET_SYSERR == ctx->is_valid) return; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (deposit_value->currency, + &total_refunds)); + qs = TEH_plugin->select_refunds_by_coin (TEH_plugin->cls, + coin_pub, + merchant_pub, + h_contract_terms, + &add_refunds, + &total_refunds); + if (qs < 0) + { + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + if (1 == + TALER_amount_cmp (&total_refunds, + deposit_value)) + { + /* Refunds exceeded total deposit? not OK! */ + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + if (0 == + TALER_amount_cmp (&total_refunds, + deposit_value)) + { + /* total_refunds == deposit_value; + in this case, the total contributed to the + wire transfer is zero (as are fees) */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (deposit_value->currency, + &dval)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (deposit_value->currency, + &dfee)); + + } + else + { + /* Compute deposit value by subtracting refunds */ + GNUNET_assert (0 < + TALER_amount_subtract (&dval, + deposit_value, + &total_refunds)); + if (-1 == + TALER_amount_cmp (&dval, + deposit_fee)) + { + /* dval < deposit_fee, so after refunds less than + the deposit fee remains; reduce deposit fee to + the remaining value of the coin */ + dfee = dval; + } + else + { + /* Partial refund, deposit fee remains */ + dfee = *deposit_fee; + } + } + if (GNUNET_NO == ctx->is_valid) { /* First one we encounter, setup general information in 'ctx' */ @@ -294,8 +397,8 @@ handle_deposit_data (void *cls, ctx->is_valid = GNUNET_YES; if (0 > TALER_amount_subtract (&ctx->total, - deposit_value, - deposit_fee)) + &dval, + &dfee)) { GNUNET_break (0); ctx->is_valid = GNUNET_SYSERR; @@ -319,8 +422,8 @@ handle_deposit_data (void *cls, } if (0 > TALER_amount_subtract (&delta, - deposit_value, - deposit_fee)) + &dval, + &dfee)) { GNUNET_break (0); ctx->is_valid = GNUNET_SYSERR; @@ -341,8 +444,9 @@ handle_deposit_data (void *cls, struct AggregatedDepositDetail *wdd; wdd = GNUNET_new (struct AggregatedDepositDetail); - wdd->deposit_value = *deposit_value; - wdd->deposit_fee = *deposit_fee; + wdd->deposit_value = dval; + wdd->deposit_fee = dfee; + wdd->refund_total = total_refunds; wdd->h_contract_terms = *h_contract_terms; wdd->coin_pub = *coin_pub; GNUNET_CONTAINER_DLL_insert (ctx->wdd_head, @@ -399,7 +503,6 @@ get_transfer_deposits (void *cls, struct GNUNET_TIME_Timestamp wire_fee_start_date; struct GNUNET_TIME_Timestamp wire_fee_end_date; struct TALER_MasterSignatureP wire_fee_master_sig; - struct TALER_Amount closing_fee; /* resetting to NULL/0 in case transaction was repeated after serialization failure */ @@ -455,8 +558,7 @@ get_transfer_deposits (void *cls, ctx->exec_time, &wire_fee_start_date, &wire_fee_end_date, - &ctx->wire_fee, - &closing_fee, + &ctx->fees, &wire_fee_master_sig); GNUNET_free (wire_method); } @@ -476,7 +578,7 @@ get_transfer_deposits (void *cls, if (0 > TALER_amount_subtract (&ctx->total, &ctx->total, - &ctx->wire_fee)) + &ctx->fees.wire)) { GNUNET_break (0); *mhd_ret = TALER_MHD_reply_with_error (connection, @@ -514,7 +616,7 @@ TEH_handler_transfers_get (struct TEH_RequestContext *rc, if (GNUNET_OK != TEH_DB_run_transaction (rc->connection, "run transfers GET", - TEH_MT_OTHER, + TEH_MT_REQUEST_OTHER, &mhd_ret, &get_transfer_deposits, &ctx)) @@ -526,7 +628,7 @@ TEH_handler_transfers_get (struct TEH_RequestContext *rc, &ctx.total, &ctx.merchant_pub, ctx.payto_uri, - &ctx.wire_fee, + &ctx.fees.wire, ctx.exec_time, ctx.wdd_head); free_ctx (&ctx); diff --git a/src/exchange/taler-exchange-httpd_wire.c b/src/exchange/taler-exchange-httpd_wire.c deleted file mode 100644 index d378bdabc..000000000 --- a/src/exchange/taler-exchange-httpd_wire.c +++ /dev/null @@ -1,425 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2015-2021 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_wire.c - * @brief Handle /wire requests - * @author Christian Grothoff - */ -#include "platform.h" -#include <gnunet/gnunet_json_lib.h> -#include "taler_dbevents.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_wire.h" -#include "taler_json_lib.h" -#include "taler_mhd_lib.h" -#include <jansson.h> - - -/** - * Stores the latest generation of our wire response. - */ -static struct WireStateHandle *wire_state; - -/** - * Handler listening for wire updates by other exchange - * services. - */ -static struct GNUNET_DB_EventHandler *wire_eh; - -/** - * Counter incremented whenever we have a reason to re-build the #wire_state - * because something external changed. - */ -static uint64_t wire_generation; - - -/** - * State we keep per thread to cache the /wire response. - */ -struct WireStateHandle -{ - /** - * Cached JSON for /wire response. - */ - json_t *wire_reply; - - /** - * For which (global) wire_generation was this data structure created? - * Used to check when we are outdated and need to be re-generated. - */ - uint64_t wire_generation; - - /** - * HTTP status to return with this response. - */ - unsigned int http_status; - -}; - - -/** - * Free memory associated with @a wsh - * - * @param[in] wsh wire state to destroy - */ -static void -destroy_wire_state (struct WireStateHandle *wsh) -{ - json_decref (wsh->wire_reply); - GNUNET_free (wsh); -} - - -/** - * Function called whenever another exchange process has updated - * the wire data in the database. - * - * @param cls NULL - * @param extra unused - * @param extra_size number of bytes in @a extra unused - */ -static void -wire_update_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - (void) cls; - (void) extra; - (void) extra_size; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received /wire update event\n"); - TEH_check_invariants (); - wire_generation++; -} - - -int -TEH_wire_init () -{ - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), - }; - - wire_eh = TEH_plugin->event_listen (TEH_plugin->cls, - GNUNET_TIME_UNIT_FOREVER_REL, - &es, - &wire_update_event_cb, - NULL); - if (NULL == wire_eh) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -void -TEH_wire_done () -{ - if (NULL != wire_state) - { - destroy_wire_state (wire_state); - wire_state = NULL; - } - if (NULL != wire_eh) - { - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - wire_eh); - wire_eh = NULL; - } -} - - -/** - * Create standard JSON response format using - * @param ec and @a detail - * - * @param ec error code to return - * @param detail optional detail text to return, can be NULL - * @return JSON response - */ -static json_t * -make_ec_reply (enum TALER_ErrorCode ec, - const char *detail) -{ - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("code", ec), - GNUNET_JSON_pack_string ("hint", - TALER_ErrorCode_get_hint (ec)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("detail", detail))); -} - - -/** - * Add information about a wire account to @a cls. - * - * @param cls a `json_t *` object to expand with wire account details - * @param payto_uri the exchange bank account URI to add - * @param master_sig master key signature affirming that this is a bank - * account of the exchange (of purpose #TALER_SIGNATURE_MASTER_WIRE_DETAILS) - */ -static void -add_wire_account (void *cls, - const char *payto_uri, - const struct TALER_MasterSignatureP *master_sig) -{ - json_t *a = cls; - - if (0 != - json_array_append_new ( - a, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("payto_uri", - payto_uri), - GNUNET_JSON_pack_data_auto ("master_sig", - master_sig)))) - { - GNUNET_break (0); /* out of memory!? */ - return; - } -} - - -/** - * Add information about a wire account to @a cls. - * - * @param cls a `json_t *` array to expand with wire account details - * @param wire_fee the wire fee we charge - * @param closing_fee the closing fee we charge - * @param start_date from when are these fees valid (start date) - * @param end_date until when are these fees valid (end date, exclusive) - * @param master_sig master key signature affirming that this is the correct - * fee (of purpose #TALER_SIGNATURE_MASTER_WIRE_FEES) - */ -static void -add_wire_fee (void *cls, - const struct TALER_Amount *wire_fee, - const struct TALER_Amount *closing_fee, - struct GNUNET_TIME_Timestamp start_date, - struct GNUNET_TIME_Timestamp end_date, - const struct TALER_MasterSignatureP *master_sig) -{ - json_t *a = cls; - - if (0 != - json_array_append_new ( - a, - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("wire_fee", - wire_fee), - TALER_JSON_pack_amount ("closing_fee", - closing_fee), - GNUNET_JSON_pack_timestamp ("start_date", - start_date), - GNUNET_JSON_pack_timestamp ("end_date", - end_date), - GNUNET_JSON_pack_data_auto ("sig", - master_sig)))) - { - GNUNET_break (0); /* out of memory!? */ - return; - } -} - - -/** - * Create the /wire response from our database state. - * - * @return NULL on error - */ -static struct WireStateHandle * -build_wire_state (void) -{ - json_t *wire_accounts_array; - json_t *wire_fee_object; - uint64_t wg = wire_generation; /* must be obtained FIRST */ - enum GNUNET_DB_QueryStatus qs; - struct WireStateHandle *wsh; - - wsh = GNUNET_new (struct WireStateHandle); - wsh->wire_generation = wg; - wire_accounts_array = json_array (); - GNUNET_assert (NULL != wire_accounts_array); - qs = TEH_plugin->get_wire_accounts (TEH_plugin->cls, - &add_wire_account, - wire_accounts_array); - if (0 > qs) - { - GNUNET_break (0); - json_decref (wire_accounts_array); - wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - wsh->wire_reply - = make_ec_reply (TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_wire_accounts"); - return wsh; - } - if (0 == json_array_size (wire_accounts_array)) - { - json_decref (wire_accounts_array); - wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - wsh->wire_reply - = make_ec_reply (TALER_EC_EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED, - NULL); - return wsh; - } - wire_fee_object = json_object (); - GNUNET_assert (NULL != wire_fee_object); - { - json_t *account; - size_t index; - - json_array_foreach (wire_accounts_array, index, account) { - char *wire_method; - const char *payto_uri = json_string_value (json_object_get (account, - "payto_uri")); - - GNUNET_assert (NULL != payto_uri); - wire_method = TALER_payto_get_method (payto_uri); - if (NULL == wire_method) - { - wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - wsh->wire_reply - = make_ec_reply (TALER_EC_EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED, - payto_uri); - json_decref (wire_accounts_array); - json_decref (wire_fee_object); - return wsh; - } - if (NULL == json_object_get (wire_fee_object, - wire_method)) - { - json_t *a = json_array (); - - GNUNET_assert (NULL != a); - qs = TEH_plugin->get_wire_fees (TEH_plugin->cls, - wire_method, - &add_wire_fee, - a); - if (0 > qs) - { - GNUNET_break (0); - json_decref (a); - json_decref (wire_fee_object); - json_decref (wire_accounts_array); - GNUNET_free (wire_method); - wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - wsh->wire_reply - = make_ec_reply (TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_wire_fees"); - return wsh; - } - if (0 == json_array_size (a)) - { - json_decref (a); - json_decref (wire_accounts_array); - json_decref (wire_fee_object); - wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - wsh->wire_reply - = make_ec_reply (TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, - wire_method); - GNUNET_free (wire_method); - return wsh; - } - GNUNET_assert (0 == - json_object_set_new (wire_fee_object, - wire_method, - a)); - } - GNUNET_free (wire_method); - } - } - wsh->wire_reply = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("accounts", - wire_accounts_array), - GNUNET_JSON_pack_object_steal ("fees", - wire_fee_object), - GNUNET_JSON_pack_data_auto ("master_public_key", - &TEH_master_public_key)); - wsh->http_status = MHD_HTTP_OK; - return wsh; -} - - -void -TEH_wire_update_state (void) -{ - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_EXCHANGE_WIRE_UPDATED), - }; - - TEH_plugin->event_notify (TEH_plugin->cls, - &es, - NULL, - 0); - wire_generation++; -} - - -/** - * Return the current key state for this thread. Possibly - * re-builds the key state if we have reason to believe - * that something changed. - * - * @return NULL on error - */ -struct WireStateHandle * -get_wire_state (void) -{ - struct WireStateHandle *old_wsh; - - old_wsh = wire_state; - if ( (NULL == old_wsh) || - (old_wsh->wire_generation < wire_generation) ) - { - struct WireStateHandle *wsh; - - TEH_check_invariants (); - wsh = build_wire_state (); - wire_state = wsh; - if (NULL != old_wsh) - destroy_wire_state (old_wsh); - TEH_check_invariants (); - return wsh; - } - return old_wsh; -} - - -MHD_RESULT -TEH_handler_wire (struct TEH_RequestContext *rc, - const char *const args[]) -{ - struct WireStateHandle *wsh; - - (void) args; - wsh = get_wire_state (); - if (NULL == wsh) - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, - NULL); - return TALER_MHD_reply_json (rc->connection, - wsh->wire_reply, - wsh->http_status); -} - - -/* end of taler-exchange-httpd_wire.c */ diff --git a/src/exchange/taler-exchange-httpd_wire.h b/src/exchange/taler-exchange-httpd_wire.h deleted file mode 100644 index ed815a57e..000000000 --- a/src/exchange/taler-exchange-httpd_wire.h +++ /dev/null @@ -1,70 +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 Affero 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_wire.h - * @brief Handle /wire requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_WIRE_H -#define TALER_EXCHANGE_HTTPD_WIRE_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Clean up wire subsystem. - */ -void -TEH_wire_done (void); - - -/** - * Initialize wire subsystem. - * - * @return #GNUNET_OK on success - */ -enum GNUNET_GenericReturnValue -TEH_wire_init (void); - - -/** - * Something changed in the database. Rebuild the wire replies. This function - * should be called if the exchange learns about a new signature from our - * master key. - * - * (We do not do so immediately, but merely signal to all threads that they - * need to rebuild their wire state upon the next call to - * #TEH_handler_wire()). - */ -void -TEH_wire_update_state (void); - - -/** - * Handle a "/wire" request. - * - * @param rc request context - * @param args array of additional options (must be empty for this function) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_wire (struct TEH_RequestContext *rc, - const char *const args[]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c deleted file mode 100644 index 53ba270ba..000000000 --- a/src/exchange/taler-exchange-httpd_withdraw.c +++ /dev/null @@ -1,543 +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 Affero 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General - Public License along with TALER; see the file COPYING. If not, - see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_withdraw.c - * @brief Handle /reserves/$RESERVE_PUB/withdraw requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler_json_lib.h" -#include "taler_mhd_lib.h" -#include "taler-exchange-httpd_withdraw.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Send reserve history information to client with the - * message that we have insufficient funds for the - * requested withdraw operation. - * - * @param connection connection to the client - * @param ebalance expected balance based on our database - * @param withdraw_amount amount that the client requested to withdraw - * @param rh reserve history to return - * @return MHD result code - */ -static MHD_RESULT -reply_withdraw_insufficient_funds ( - struct MHD_Connection *connection, - const struct TALER_Amount *ebalance, - const struct TALER_Amount *withdraw_amount, - const struct TALER_EXCHANGEDB_ReserveHistory *rh) -{ - json_t *json_history; - struct TALER_Amount balance; - - json_history = TEH_RESPONSE_compile_reserve_history (rh, - &balance); - if (NULL == json_history) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WITHDRAW_HISTORY_ERROR_INSUFFICIENT_FUNDS, - NULL); - if (0 != - TALER_amount_cmp (&balance, - ebalance)) - { - GNUNET_break (0); - json_decref (json_history); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "reserve balance corrupt"); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_CONFLICT, - TALER_JSON_pack_ec (TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS), - TALER_JSON_pack_amount ("balance", - &balance), - TALER_JSON_pack_amount ("requested_amount", - withdraw_amount), - GNUNET_JSON_pack_array_steal ("history", - json_history)); -} - - -/** - * Context for #withdraw_transaction. - */ -struct WithdrawContext -{ - /** - * Details about the withdrawal request. - */ - struct TALER_WithdrawRequestPS wsrd; - - /** - * Blinded planchet. - */ - void *blinded_msg; - - /** - * Number of bytes in @e blinded_msg. - */ - size_t blinded_msg_len; - - /** - * Set to the resulting signed coin data to be returned to the client. - */ - struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; - - /** - * KYC status for the operation. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - -}; - - -/** - * Function implementing withdraw transaction. Runs the - * transaction logic; IF it returns a non-error code, the transaction - * logic MUST NOT queue a MHD response. IF it returns an hard error, - * the transaction logic MUST queue a MHD response and set @a mhd_ret. - * IF it returns the soft error code, the function MAY be called again - * to retry and MUST not queue a MHD response. - * - * Note that "wc->collectable.sig" is set before entering this function as we - * signed before entering the transaction. - * - * @param cls a `struct WithdrawContext *` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -withdraw_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct WithdrawContext *wc = cls; - enum GNUNET_DB_QueryStatus qs; - bool found = false; - bool balance_ok = false; - struct GNUNET_TIME_Timestamp now; - uint64_t ruuid; - - now = GNUNET_TIME_timestamp_get (); - wc->collectable.reserve_pub = wc->wsrd.reserve_pub; - wc->collectable.h_coin_envelope = wc->wsrd.h_coin_envelope; - qs = TEH_plugin->do_withdraw (TEH_plugin->cls, - &wc->collectable, - now, - &found, - &balance_ok, - &wc->kyc, - &ruuid); - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_withdraw"); - return qs; - } - if (! found) - { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_WITHDRAW_RESERVE_UNKNOWN, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - struct TALER_EXCHANGEDB_ReserveHistory *rh; - struct TALER_Amount balance; - struct TALER_Amount requested_amount; - - TEH_plugin->rollback (TEH_plugin->cls); - // FIXME: maybe start read-committed here? - if (GNUNET_OK != - TEH_plugin->start (TEH_plugin->cls, - "get_reserve_history on insufficient balance")) - { - GNUNET_break (0); - if (NULL != mhd_ret) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - /* The reserve does not have the required amount (actual - * amount + withdraw fee) */ - qs = TEH_plugin->get_reserve_history (TEH_plugin->cls, - &wc->wsrd.reserve_pub, - &balance, - &rh); - if (NULL == rh) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "reserve history"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - TALER_amount_ntoh (&requested_amount, - &wc->wsrd.amount_with_fee); - *mhd_ret = reply_withdraw_insufficient_funds (connection, - &balance, - &requested_amount, - rh); - TEH_plugin->free_reserve_history (TEH_plugin->cls, - rh); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (! wc->kyc.ok) && - (TALER_EXCHANGEDB_KYC_W2W == wc->kyc.type) ) - { - /* Wallet-to-wallet payments _always_ require KYC */ - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - wc->kyc.payment_target_uuid)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (! wc->kyc.ok) && - (TALER_EXCHANGEDB_KYC_WITHDRAW == wc->kyc.type) && - (! GNUNET_TIME_relative_is_zero (TEH_kyc_config.withdraw_period)) ) - { - /* Withdraws require KYC if above threshold */ - enum GNUNET_DB_QueryStatus qs2; - bool below_limit; - - qs2 = TEH_plugin->do_withdraw_limit_check ( - TEH_plugin->cls, - ruuid, - GNUNET_TIME_absolute_subtract (now.abs_time, - TEH_kyc_config.withdraw_period), - &TEH_kyc_config.withdraw_limit, - &below_limit); - if (0 > qs2) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs2); - if (GNUNET_DB_STATUS_HARD_ERROR == qs2) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_withdraw_limit_check"); - return qs2; - } - if (! below_limit) - { - TEH_plugin->rollback (TEH_plugin->cls); - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - wc->kyc.payment_target_uuid)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - return qs; -} - - -/** - * Check if the @a rc is replayed and we already have an - * answer. If so, replay the existing answer and return the - * HTTP response. - * - * @param rc request context - * @param[in,out] wc parsed request data - * @param[out] mret HTTP status, set if we return true - * @return true if the request is idempotent with an existing request - * false if we did not find the request in the DB and did not set @a mret - */ -static bool -check_request_idempotent (struct TEH_RequestContext *rc, - struct WithdrawContext *wc, - MHD_RESULT *mret) -{ - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_withdraw_info (TEH_plugin->cls, - &wc->wsrd.h_coin_envelope, - &wc->collectable); - if (0 > qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mret = TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_withdraw_info"); - return true; /* well, kind-of */ - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return false; - /* generate idempotent reply */ - *mret = TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - TALER_JSON_pack_blinded_denom_sig ("ev_sig", - &wc->collectable.sig)); - TALER_blinded_denom_sig_free (&wc->collectable.sig); - return true; -} - - -MHD_RESULT -TEH_handler_withdraw (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[2]) -{ - struct WithdrawContext wc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_varsize ("coin_ev", - &wc.blinded_msg, - &wc.blinded_msg_len), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &wc.collectable.reserve_sig), - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &wc.collectable.denom_pub_hash), - GNUNET_JSON_spec_end () - }; - enum TALER_ErrorCode ec; - struct TEH_DenominationKey *dk; - - memset (&wc, - 0, - sizeof (wc)); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &wc.wsrd.reserve_pub, - sizeof (wc.wsrd.reserve_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_GENERIC_RESERVE_PUB_MALFORMED, - args[0]); - } - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_OK != res) - return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; - } - { - MHD_RESULT mret; - struct TEH_KeyStateHandle *ksh; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - if (! check_request_idempotent (rc, - &wc, - &mret)) - { - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - } - GNUNET_JSON_parse_free (spec); - return mret; - } - dk = TEH_keys_denomination_by_hash2 (ksh, - &wc.collectable.denom_pub_hash, - NULL, - NULL); - if (NULL == dk) - { - if (! check_request_idempotent (rc, - &wc, - &mret)) - { - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_unknown_denom_pub_hash ( - rc->connection, - &wc.collectable.denom_pub_hash); - } - GNUNET_JSON_parse_free (spec); - return mret; - } - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) - { - /* This denomination is past the expiration time for withdraws */ - if (! check_request_idempotent (rc, - &wc, - &mret)) - { - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &wc.collectable.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "WITHDRAW"); - } - GNUNET_JSON_parse_free (spec); - return mret; - } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) - { - /* This denomination is not yet valid, no need to check - for idempotency! */ - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &wc.collectable.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "WITHDRAW"); - } - if (dk->recoup_possible) - { - /* This denomination has been revoked */ - if (! check_request_idempotent (rc, - &wc, - &mret)) - { - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &wc.collectable.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - "WITHDRAW"); - } - GNUNET_JSON_parse_free (spec); - return mret; - } - } - - if (0 > - TALER_amount_add (&wc.collectable.amount_with_fee, - &dk->meta.value, - &dk->meta.fee_withdraw)) - { - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, - NULL); - } - TALER_amount_hton (&wc.wsrd.amount_with_fee, - &wc.collectable.amount_with_fee); - - // FIXME: move this logic into libtalerutil! - /* verify signature! */ - wc.wsrd.purpose.size - = htonl (sizeof (wc.wsrd)); - wc.wsrd.purpose.purpose - = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW); - wc.wsrd.h_denomination_pub - = wc.collectable.denom_pub_hash; - TALER_coin_ev_hash (wc.blinded_msg, - wc.blinded_msg_len, - &wc.wsrd.h_coin_envelope); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW, - &wc.wsrd, - &wc.collectable.reserve_sig.eddsa_signature, - &wc.wsrd.reserve_pub.eddsa_pub)) - { - TALER_LOG_WARNING ( - "Client supplied invalid signature for withdraw request\n"); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, - NULL); - } - - /* Sign before transaction! */ - ec = TALER_EC_NONE; - wc.collectable.sig - = TEH_keys_denomination_sign (&wc.collectable.denom_pub_hash, - wc.blinded_msg, - wc.blinded_msg_len, - &ec); - if (TALER_EC_NONE != ec) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_ec (rc->connection, - ec, - NULL); - } - - /* run transaction */ - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "run withdraw", - TEH_MT_WITHDRAW, - &mhd_ret, - &withdraw_transaction, - &wc)) - { - /* Even if #withdraw_transaction() failed, it may have created a signature - (or we might have done it optimistically above). */ - TALER_blinded_denom_sig_free (&wc.collectable.sig); - GNUNET_JSON_parse_free (spec); - return mhd_ret; - } - } - - /* Clean up and send back final response */ - GNUNET_JSON_parse_free (spec); - - { - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - TALER_JSON_pack_blinded_denom_sig ("ev_sig", - &wc.collectable.sig)); - TALER_blinded_denom_sig_free (&wc.collectable.sig); - return ret; - } -} - - -/* end of taler-exchange-httpd_withdraw.c */ diff --git a/src/exchange/taler-exchange-kyc-aml-pep-trigger.sh b/src/exchange/taler-exchange-kyc-aml-pep-trigger.sh new file mode 100755 index 000000000..9baa32baf --- /dev/null +++ b/src/exchange/taler-exchange-kyc-aml-pep-trigger.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# This file is in the public domain. +# This is an example of how to trigger AML if the +# KYC attributes include '{"pep":true}' +# +# To be used as a script for the KYC_AML_TRIGGER. +test "false" = $(jq .pep -) diff --git a/src/exchange/taler-exchange-router.c b/src/exchange/taler-exchange-router.c new file mode 100644 index 000000000..a1a247194 --- /dev/null +++ b/src/exchange/taler-exchange-router.c @@ -0,0 +1,450 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +/** + * @file taler-exchange-router.c + * @brief Process that routes P2P payments. Responsible for + * aggregating remote payments into the respective wad transfers. + * Execution of actual wad transfers is still to be done by taler-exchange-transfer, + * and watching for incoming wad transfers is done by taler-exchange-wirewatch. + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <pthread.h> +#include "taler_exchangedb_lib.h" +#include "taler_exchangedb_plugin.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" + + +// FIXME #7271: revisit how (and if) we do sharding! +// Maybe use different helpers for wads than +// for local purses?! +/** + * Work shard we are processing. + */ +struct Shard +{ + + /** + * When did we start processing the shard? + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * Starting row of the shard. + */ + uint32_t shard_start; + + /** + * Inclusive end row of the shard. + */ + uint32_t shard_end; + + /** + * Number of starting points found in the shard. + */ + uint64_t work_counter; + +}; + + +/** + * What is the smallest unit we support for wire transfers? + * We will need to round down to a multiple of this amount. + */ +static struct TALER_Amount currency_round_unit; + +/** + * What is the base URL of this exchange? Used in the + * wire transfer subjects so that merchants and governments + * can ask for the list of aggregated deposits. + */ +static char *exchange_base_url; + +/** + * Set to #GNUNET_YES if this exchange does not support KYC checks + * and thus P2P transfers are to be made regardless of the + * KYC status of the target reserve. + */ +static int kyc_off; + +/** + * The exchange's configuration. + */ +static const struct GNUNET_CONFIGURATION_Handle *cfg; + +/** + * Our database plugin. + */ +static struct TALER_EXCHANGEDB_Plugin *db_plugin; + +/** + * Next task to run, if any. + */ +static struct GNUNET_SCHEDULER_Task *task; + +/** + * How long should we sleep when idle before trying to find more work? + */ +static struct GNUNET_TIME_Relative router_idle_sleep_interval; + +/** + * How big are the shards we are processing? Is an inclusive offset, so every + * shard ranges from [X,X+shard_size) exclusive. So a shard covers + * shard_size slots. The maximum value for shard_size is INT32_MAX+1. + */ +static uint32_t shard_size; + +/** + * Value to return from main(). 0 on success, non-zero on errors. + */ +static int global_ret; + +/** + * #GNUNET_YES if we are in test mode and should exit when idle. + */ +static int test_mode; + + +/** + * Select a shard to work on. + * + * @param cls NULL + */ +static void +run_shard (void *cls); + + +/** + * We're being aborted with CTRL-C (or SIGTERM). Shut down. + * + * @param cls closure + */ +static void +shutdown_task (void *cls) +{ + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running shutdown\n"); + if (NULL != task) + { + GNUNET_SCHEDULER_cancel (task); + task = NULL; + } + TALER_EXCHANGEDB_plugin_unload (db_plugin); + db_plugin = NULL; + TALER_EXCHANGEDB_unload_accounts (); + cfg = NULL; +} + + +/** + * Parse the configuration for wirewatch. + * + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_wirewatch_config (void) +{ + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "BASE_URL", + &exchange_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + "exchange", + "ROUTER_IDLE_SLEEP_INTERVAL", + &router_idle_sleep_interval)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "ROUTER_IDLE_SLEEP_INTERVAL"); + return GNUNET_SYSERR; + } + if ( (GNUNET_OK != + TALER_config_get_amount (cfg, + "taler", + "CURRENCY_ROUND_UNIT", + ¤cy_round_unit)) || + ( (0 != currency_round_unit.fraction) && + (0 != currency_round_unit.value) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Need non-zero value in section `TALER' under `CURRENCY_ROUND_UNIT'\n"); + return GNUNET_SYSERR; + } + + if (NULL == + (db_plugin = TALER_EXCHANGEDB_plugin_load (cfg))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to initialize DB subsystem\n"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_EXCHANGEDB_load_accounts (cfg, + TALER_EXCHANGEDB_ALO_DEBIT)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No wire accounts configured for debit!\n"); + TALER_EXCHANGEDB_plugin_unload (db_plugin); + db_plugin = NULL; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Perform a database commit. If it fails, print a warning. + * + * @return status of commit + */ +static enum GNUNET_DB_QueryStatus +commit_or_warn (void) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->commit (db_plugin->cls); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return qs; + GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? GNUNET_ERROR_TYPE_INFO + : GNUNET_ERROR_TYPE_ERROR, + "Failed to commit database transaction!\n"); + return qs; +} + + +/** + * Release lock on shard @a s in the database. + * On error, terminates this process. + * + * @param[in] s shard to free (and memory to release) + */ +static void +release_shard (struct Shard *s) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->release_revolving_shard ( + db_plugin->cls, + "router", + s->shard_start, + s->shard_end); + GNUNET_free (s); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Strange, but let's just continue */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* normal case */ + break; + } +} + + +static void +run_routing (void *cls) +{ + struct Shard *s = cls; + + task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking for ready P2P transfers to route\n"); + // FIXME #7271: do actual work here! + commit_or_warn (); + release_shard (s); + task = GNUNET_SCHEDULER_add_now (&run_shard, + NULL); +} + + +/** + * Select a shard to work on. + * + * @param cls NULL + */ +static void +run_shard (void *cls) +{ + struct Shard *s; + enum GNUNET_DB_QueryStatus qs; + + (void) cls; + task = NULL; + if (GNUNET_SYSERR == + db_plugin->preflight (db_plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to obtain database connection!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + s = GNUNET_new (struct Shard); + s->start_time = GNUNET_TIME_timestamp_get (); + qs = db_plugin->begin_revolving_shard (db_plugin->cls, + "router", + shard_size, + 1U + INT32_MAX, + &s->shard_start, + &s->shard_end); + if (0 >= qs) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + static struct GNUNET_TIME_Relative delay; + + GNUNET_free (s); + delay = GNUNET_TIME_randomized_backoff (delay, + GNUNET_TIME_UNIT_SECONDS); + task = GNUNET_SCHEDULER_add_delayed (delay, + &run_shard, + NULL); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to begin shard (%d)!\n", + qs); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting shard [%u:%u]!\n", + (unsigned int) s->shard_start, + (unsigned int) s->shard_end); + task = GNUNET_SCHEDULER_add_now (&run_routing, + s); +} + + +/** + * First task. + * + * @param cls closure, NULL + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + unsigned long long ass; + (void) cls; + (void) args; + (void) cfgfile; + + cfg = c; + if (GNUNET_OK != parse_wirewatch_config ()) + { + cfg = NULL; + global_ret = EXIT_NOTCONFIGURED; + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (cfg, + "exchange", + "ROUTER_SHARD_SIZE", + &ass)) + { + cfg = NULL; + global_ret = EXIT_NOTCONFIGURED; + return; + } + if ( (0 == ass) || + (ass > INT32_MAX) ) + shard_size = 1U + INT32_MAX; + else + shard_size = (uint32_t) ass; + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_shard, + NULL); + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + cls); +} + + +/** + * The main function of the taler-exchange-router. + * + * @param argc number of arguments from the command line + * @param argv command line arguments + * @return 0 ok, non-zero on error, see #global_ret + */ +int +main (int argc, + char *const *argv) +{ + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_timetravel ('T', + "timetravel"), + GNUNET_GETOPT_option_flag ('t', + "test", + "run in test mode and exit when idle", + &test_mode), + GNUNET_GETOPT_option_flag ('y', + "kyc-off", + "perform wire transfers without KYC checks", + &kyc_off), + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + if (GNUNET_OK != + GNUNET_STRINGS_get_utf8_args (argc, argv, + &argc, &argv)) + return EXIT_INVALIDARGUMENT; + TALER_OS_init (); + ret = GNUNET_PROGRAM_run ( + argc, argv, + "taler-exchange-router", + gettext_noop ( + "background process that routes P2P transfers"), + options, + &run, NULL); + GNUNET_free_nz ((void *) argv); + if (GNUNET_SYSERR == ret) + return EXIT_INVALIDARGUMENT; + if (GNUNET_NO == ret) + return EXIT_SUCCESS; + return global_ret; +} + + +/* end of taler-exchange-router.c */ diff --git a/src/exchange/taler-exchange-transfer.c b/src/exchange/taler-exchange-transfer.c index 011da6b57..9724b41fc 100644 --- a/src/exchange/taler-exchange-transfer.c +++ b/src/exchange/taler-exchange-transfer.c @@ -264,7 +264,7 @@ shutdown_task (void *cls) /** - * Parse the configuration for wirewatch. + * Parse the configuration for taler-exchange-transfer. * * @return #GNUNET_OK on success */ @@ -406,25 +406,17 @@ batch_done (void) * except for irrecoverable errors. * * @param cls `struct WirePrepareData` we are working on - * @param http_status_code #MHD_HTTP_OK on success - * @param ec taler error code - * @param row_id unique ID of the wire transfer in the bank's records - * @param wire_timestamp when did the transfer happen + * @param tr transfer response */ static void wire_confirm_cb (void *cls, - unsigned int http_status_code, - enum TALER_ErrorCode ec, - uint64_t row_id, - struct GNUNET_TIME_Timestamp wire_timestamp) + const struct TALER_BANK_TransferResponse *tr) { struct WirePrepareData *wpd = cls; enum GNUNET_DB_QueryStatus qs; - (void) row_id; - (void) wire_timestamp; wpd->eh = NULL; - switch (http_status_code) + switch (tr->http_status) { case MHD_HTTP_OK: GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -435,11 +427,12 @@ wire_confirm_cb (void *cls, /* continued below */ break; case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_CONFLICT: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Wire transaction %llu failed: %u/%d\n", (unsigned long long) wpd->row_id, - http_status_code, - ec); + tr->http_status, + tr->ec); qs = db_plugin->wire_prepare_data_mark_failed (db_plugin->cls, wpd->row_id); /* continued below */ @@ -456,7 +449,7 @@ wire_confirm_cb (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Wire transfer %llu failed (%u), trying again\n", (unsigned long long) wpd->row_id, - http_status_code); + tr->http_status); wpd->eh = TALER_BANK_transfer (ctx, wpd->wa->auth, &wpd[1], @@ -468,8 +461,8 @@ wire_confirm_cb (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Wire transaction %llu failed: %u/%d\n", (unsigned long long) wpd->row_id, - http_status_code, - ec); + tr->http_status, + tr->ec); cleanup_wpd (); db_plugin->rollback (db_plugin->cls); global_ret = EXIT_FAILURE; @@ -479,8 +472,8 @@ wire_confirm_cb (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Wire transfer %llu failed: %u/%d\n", (unsigned long long) wpd->row_id, - http_status_code, - ec); + tr->http_status, + tr->ec); db_plugin->rollback (db_plugin->cls); cleanup_wpd (); global_ret = EXIT_FAILURE; @@ -563,9 +556,9 @@ wire_prepare_cb (void *cls, } wpd = GNUNET_malloc (sizeof (struct WirePrepareData) + buf_size); - memcpy (&wpd[1], - buf, - buf_size); + GNUNET_memcpy (&wpd[1], + buf, + buf_size); wpd->buf_size = buf_size; wpd->row_id = rowid; GNUNET_CONTAINER_DLL_insert (wpd_head, @@ -582,7 +575,7 @@ wire_prepare_cb (void *cls, GNUNET_break (0); cleanup_wpd (); db_plugin->rollback (db_plugin->cls); - global_ret = EXIT_NOTCONFIGURED; + global_ret = EXIT_NO_RESTART; GNUNET_SCHEDULER_shutdown (); return; } @@ -715,7 +708,7 @@ run_transfers (void *cls) } else { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "No more pending wire transfers, going idle\n"); GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_delayed (transfer_idle_sleep_interval, diff --git a/src/exchange/taler-exchange-wirewatch.c b/src/exchange/taler-exchange-wirewatch.c index 6b63de76a..da5d9c098 100644 --- a/src/exchange/taler-exchange-wirewatch.c +++ b/src/exchange/taler-exchange-wirewatch.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2016--2021 Taler Systems SA + Copyright (C) 2016--2023 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -13,7 +13,6 @@ You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** * @file taler-exchange-wirewatch.c * @brief Process that watches for wire transfers to the exchange's bank account @@ -43,111 +42,111 @@ #define MAXIMUM_BATCH_SIZE 1024 /** - * Information we keep for each supported account. + * Information about our account. */ -struct WireAccount -{ - /** - * Accounts are kept in a DLL. - */ - struct WireAccount *next; - - /** - * Plugins are kept in a DLL. - */ - struct WireAccount *prev; +static const struct TALER_EXCHANGEDB_AccountInfo *ai; - /** - * Information about this account. - */ - const struct TALER_EXCHANGEDB_AccountInfo *ai; +/** + * Active request for history. + */ +static struct TALER_BANK_CreditHistoryHandle *hh; - /** - * Active request for history. - */ - struct TALER_BANK_CreditHistoryHandle *hh; +/** + * Set to true if the request for history did actually + * return transaction items. + */ +static bool hh_returned_data; - /** - * Until when is processing this wire plugin delayed? - */ - struct GNUNET_TIME_Absolute delayed_until; +/** + * Set to true if the request for history did not + * succeed because the account was unknown. + */ +static bool hh_account_404; - /** - * Encoded offset in the wire transfer list from where - * to start the next query with the bank. - */ - uint64_t batch_start; +/** + * When did we start the last @e hh request? + */ +static struct GNUNET_TIME_Absolute hh_start_time; - /** - * Latest row offset seen in this transaction, becomes - * the new #batch_start upon commit. - */ - uint64_t latest_row_off; +/** + * Until when is processing this wire plugin delayed? + */ +static struct GNUNET_TIME_Absolute delayed_until; - /** - * Offset where our current shard begins (inclusive). - */ - uint64_t shard_start; +/** + * Encoded offset in the wire transfer list from where + * to start the next query with the bank. + */ +static uint64_t batch_start; - /** - * Offset where our current shard ends (exclusive). - */ - uint64_t shard_end; +/** + * Latest row offset seen in this transaction, becomes + * the new #batch_start upon commit. + */ +static uint64_t latest_row_off; - /** - * When did we start with the shard? - */ - struct GNUNET_TIME_Absolute shard_start_time; +/** + * Offset where our current shard begins (inclusive). + */ +static uint64_t shard_start; - /** - * Name of our job in the shard table. - */ - char *job_name; +/** + * Offset where our current shard ends (exclusive). + */ +static uint64_t shard_end; - /** - * How many transactions do we retrieve per batch? - */ - unsigned int batch_size; +/** + * When did we start with the shard? + */ +static struct GNUNET_TIME_Absolute shard_start_time; - /** - * How much do we incremnt @e batch_size on success? - */ - unsigned int batch_thresh; +/** + * For how long did we lock the shard? + */ +static struct GNUNET_TIME_Absolute shard_end_time; - /** - * How many transactions did we see in the current batch? - */ - unsigned int current_batch_size; +/** + * How long did we take to finish the last shard + * for this account? + */ +static struct GNUNET_TIME_Relative shard_delay; - /** - * Should we delay the next request to the wire plugin a bit? Set to - * false if we actually did some work. - */ - bool delay; +/** + * How long did we take to finish the last shard + * for this account? + */ +static struct GNUNET_TIME_Relative longpoll_timeout; - /** - * Did we start a transaction yet? - */ - bool started_transaction; +/** + * Name of our job in the shard table. + */ +static char *job_name; -}; +/** + * How many transactions do we retrieve per batch? + */ +static unsigned int batch_size; +/** + * How much do we increment @e batch_size on success? + */ +static unsigned int batch_thresh; /** - * Head of list of loaded wire plugins. + * Did work remain in the transaction queue? Set to true + * if we did some work and thus there might be more. */ -static struct WireAccount *wa_head; +static bool progress; /** - * Tail of list of loaded wire plugins. + * Did we start a transaction yet? */ -static struct WireAccount *wa_tail; +static bool started_transaction; /** - * Wire account we are currently processing. This would go away - * if we ever start processing all accounts in parallel. + * Is this shard still open for processing. */ -static struct WireAccount *wa_pos; +static bool shard_open; /** * Handle to the context for interacting with the bank. @@ -178,9 +177,9 @@ static struct TALER_EXCHANGEDB_Plugin *db_plugin; static struct GNUNET_TIME_Relative wirewatch_idle_sleep_interval; /** - * How long did we take to finish the last shard? + * How long do we sleep on serialization conflicts? */ -static struct GNUNET_TIME_Relative shard_delay; +static struct GNUNET_TIME_Relative wirewatch_conflict_sleep_interval; /** * Modulus to apply to group shards. The shard size must ultimately be a @@ -194,6 +193,10 @@ static unsigned int shard_size = MAXIMUM_BATCH_SIZE; */ static unsigned int max_workers = 16; +/** + * -e command-line option: exit on errors talking to the bank? + */ +static int exit_on_error; /** * Value to return from main(). 0 on success, non-zero on @@ -207,10 +210,20 @@ static int global_ret; static int test_mode; /** + * Should we ignore if the bank does not know our bank + * account? + */ +static int ignore_account_404; + +/** * Current task waiting for execution, if any. */ static struct GNUNET_SCHEDULER_Task *task; +/** + * Name of the configuration section with the account we should watch. + */ +static char *account_section; /** * We're being aborted with CTRL-C (or SIGTERM). Shut down. @@ -220,32 +233,32 @@ static struct GNUNET_SCHEDULER_Task *task; static void shutdown_task (void *cls) { + enum GNUNET_DB_QueryStatus qs; (void) cls; - { - struct WireAccount *wa; - while (NULL != (wa = wa_head)) - { - if (NULL != wa->hh) - { - TALER_BANK_credit_history_cancel (wa->hh); - wa->hh = NULL; - } - GNUNET_CONTAINER_DLL_remove (wa_head, - wa_tail, - wa); - if (wa->started_transaction) - { - db_plugin->rollback (db_plugin->cls); - wa->started_transaction = false; - } - // FIXME: delete shard lock here (#7124) - GNUNET_free (wa->job_name); - GNUNET_free (wa); - } + if (NULL != hh) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "History request cancelled on shutdown\n"); + TALER_BANK_credit_history_cancel (hh); + hh = NULL; } - wa_pos = NULL; - + if (started_transaction) + { + db_plugin->rollback (db_plugin->cls); + started_transaction = false; + } + if (shard_open) + { + qs = db_plugin->abort_shard (db_plugin->cls, + job_name, + shard_start, + shard_end); + if (qs <= 0) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to abort work shard on shutdown\n"); + } + GNUNET_free (job_name); if (NULL != ctx) { GNUNET_CURL_fini (ctx); @@ -273,28 +286,36 @@ shutdown_task (void *cls) * account to our list (if it is enabled and we can load the plugin). * * @param cls closure, NULL - * @param ai account information + * @param in_ai account information */ static void add_account_cb (void *cls, - const struct TALER_EXCHANGEDB_AccountInfo *ai) + const struct TALER_EXCHANGEDB_AccountInfo *in_ai) { - struct WireAccount *wa; - (void) cls; - if (! ai->credit_enabled) + if (! in_ai->credit_enabled) return; /* not enabled for us, skip */ - wa = GNUNET_new (struct WireAccount); - wa->ai = ai; - GNUNET_asprintf (&wa->job_name, + if ( (NULL != account_section) && + (0 != strcasecmp (in_ai->section_name, + account_section)) ) + return; /* not enabled for us, skip */ + if (NULL != ai) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Multiple accounts enabled (%s and %s), use '-a' command-line option to select one!\n", + ai->section_name, + in_ai->section_name); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_INVALIDARGUMENT; + return; + } + ai = in_ai; + GNUNET_asprintf (&job_name, "wirewatch-%s", ai->section_name); - wa->batch_size = MAXIMUM_BATCH_SIZE; - if (0 != shard_size % wa->batch_size) - wa->batch_size = shard_size; - GNUNET_CONTAINER_DLL_insert (wa_head, - wa_tail, - wa); + batch_size = MAXIMUM_BATCH_SIZE; + if (0 != shard_size % batch_size) + batch_size = shard_size; } @@ -332,342 +353,438 @@ exchange_serve_process_config (void) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No wire accounts configured for credit!\n"); - TALER_EXCHANGEDB_plugin_unload (db_plugin); - db_plugin = NULL; return GNUNET_SYSERR; } TALER_EXCHANGEDB_find_accounts (&add_account_cb, NULL); - GNUNET_assert (NULL != wa_head); + if (NULL == ai) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No accounts enabled for credit!\n"); + GNUNET_SCHEDULER_shutdown (); + return GNUNET_SYSERR; + } return GNUNET_OK; } /** - * Query for incoming wire transfers. + * Lock a shard and then begin to query for incoming wire transfers. * * @param cls NULL */ static void -find_transfers (void *cls); +lock_shard (void *cls); /** - * We encountered a serialization error. - * Rollback the transaction and try again + * Continue with the credit history of the shard. * - * @param wa account we are transacting on + * @param cls NULL */ static void -handle_soft_error (struct WireAccount *wa) +continue_with_shard (void *cls); + + +/** + * We encountered a serialization error. Rollback the transaction and try + * again. + */ +static void +handle_soft_error (void) { db_plugin->rollback (db_plugin->cls); - wa->started_transaction = false; - if (1 < wa->batch_size) + started_transaction = false; + if (1 < batch_size) { - wa->batch_thresh = wa->batch_size; - wa->batch_size /= 2; + batch_thresh = batch_size; + batch_size /= 2; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Reduced batch size to %llu due to serialization issue\n", - (unsigned long long) wa->batch_size); + (unsigned long long) batch_size); } + /* Reset to beginning of transaction, and go again + from there. */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Encountered soft error, resetting start point to batch start\n"); + latest_row_off = batch_start; GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&find_transfers, + task = GNUNET_SCHEDULER_add_now (&continue_with_shard, NULL); } /** - * We are done with a shard, move on to the next one. - * - * @param wa wire account for which we completed a shard + * Schedule the #lock_shard() operation. */ static void -shard_completed (struct WireAccount *wa) +schedule_transfers (void) { - /* transaction success, update #last_row_off */ - wa->batch_start = wa->latest_row_off; - if (wa->batch_size < MAXIMUM_BATCH_SIZE) - { - int delta; - - delta = ((int) wa->batch_thresh - (int) wa->batch_size) / 4; - if (delta < 0) - delta = -delta; - wa->batch_size = GNUNET_MIN (MAXIMUM_BATCH_SIZE, - wa->batch_size + delta + 1); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Increasing batch size to %llu\n", - (unsigned long long) wa->batch_size); - } - if (wa->delay) - { - wa->delayed_until - = GNUNET_TIME_relative_to_absolute (wirewatch_idle_sleep_interval); - wa_pos = wa_pos->next; - if (NULL == wa_pos) - wa_pos = wa_head; - GNUNET_assert (NULL != wa_pos); - } + if (shard_open) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Will retry my shard (%llu,%llu] of %s in %s\n", + (unsigned long long) shard_start, + (unsigned long long) shard_end, + job_name, + GNUNET_STRINGS_relative_time_to_string ( + GNUNET_TIME_absolute_get_remaining (delayed_until), + true)); + else + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Will try to lock next shard of %s in %s\n", + job_name, + GNUNET_STRINGS_relative_time_to_string ( + GNUNET_TIME_absolute_get_remaining (delayed_until), + true)); GNUNET_assert (NULL == task); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Will look for more transfers in %s\n", - GNUNET_STRINGS_relative_time_to_string ( - GNUNET_TIME_absolute_get_remaining (wa_pos->delayed_until), - GNUNET_YES)); - task = GNUNET_SCHEDULER_add_at (wa_pos->delayed_until, - &find_transfers, + task = GNUNET_SCHEDULER_add_at (delayed_until, + &lock_shard, NULL); } /** - * We are finished with the current shard. Update the database, marking the - * shard as finished. - * - * @param wa wire account to commit for - * @return true on success + * We are done with the work that is possible right now (and the transaction + * was committed, if there was one to commit). Move on to the next shard. */ -static bool -mark_shard_done (struct WireAccount *wa) +static void +transaction_completed (void) { - enum GNUNET_DB_QueryStatus qs; - - if (wa->shard_end > wa->latest_row_off) - return false; /* actually, not done! */ - /* shard is complete, mark this as well */ - qs = db_plugin->complete_shard (db_plugin->cls, - wa->job_name, - wa->shard_start, - wa->shard_end); - switch (qs) + if ( (batch_start + batch_size == + latest_row_off) && + (batch_size < MAXIMUM_BATCH_SIZE) ) { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - db_plugin->rollback (db_plugin->cls); - GNUNET_SCHEDULER_shutdown (); - return false; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got DB soft error for complete_shard. Rolling back.\n"); - handle_soft_error (wa); - return false; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* already existed, ok, let's just continue */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* normal case */ - shard_delay = GNUNET_TIME_absolute_get_duration (wa->shard_start_time); + /* The current batch size worked without serialization + issues, and we are allowed to grow. Do so slowly. */ + int delta; - break; + delta = ((int) batch_thresh - (int) batch_size) / 4; + if (delta < 0) + delta = -delta; + batch_size = GNUNET_MIN (MAXIMUM_BATCH_SIZE, + batch_size + delta + 1); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Increasing batch size to %llu\n", + (unsigned long long) batch_size); } - return true; -} - -/** - * We are finished with the current transaction, try - * to commit and then schedule the next iteration. - * - * @param wa wire account to commit for - */ -static void -do_commit (struct WireAccount *wa) -{ - enum GNUNET_DB_QueryStatus qs; - - wa->started_transaction = false; - mark_shard_done (wa); - qs = db_plugin->commit (db_plugin->cls); - switch (qs) + if ( (! progress) && test_mode) { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); + /* Transaction list was drained and we are in + test mode. So we are done. */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Transaction list drained and in test mode. Exiting\n"); GNUNET_SCHEDULER_shutdown (); return; - case GNUNET_DB_STATUS_SOFT_ERROR: - /* reduce transaction size to reduce rollback probability */ - handle_soft_error (wa); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* normal case */ - break; } - shard_completed (wa); + if (! (hh_returned_data || hh_account_404) ) + { + /* Enforce long-polling delay even if the server ignored it + and returned earlier */ + struct GNUNET_TIME_Relative latency; + struct GNUNET_TIME_Relative left; + + latency = GNUNET_TIME_absolute_get_duration (hh_start_time); + left = GNUNET_TIME_relative_subtract (longpoll_timeout, + latency); + if (! (test_mode || + GNUNET_TIME_relative_is_zero (left)) ) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Server did not respect long-polling, enforcing client-side by sleeping for %s\n", + GNUNET_TIME_relative2s (left, + true)); + delayed_until = GNUNET_TIME_relative_to_absolute (left); + } + if (hh_account_404) + delayed_until = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_MILLISECONDS); + if (test_mode) + delayed_until = GNUNET_TIME_UNIT_ZERO_ABS; + GNUNET_assert (NULL == task); + schedule_transfers (); } /** - * Callbacks of this type are used to serve the result of asking - * the bank for the transaction history. + * We got incoming transaction details from the bank. Add them + * to the database. * - * @param cls closure with the `struct WioreAccount *` we are processing - * @param http_status HTTP status code from the server - * @param ec taler error code - * @param serial_id identification of the position at which we are querying - * @param details details about the wire transfer - * @param json raw JSON response - * @return #GNUNET_OK to continue, #GNUNET_SYSERR to abort iteration + * @param details array of transaction details + * @param details_length length of the @a details array */ -static enum GNUNET_GenericReturnValue -history_cb (void *cls, - unsigned int http_status, - enum TALER_ErrorCode ec, - uint64_t serial_id, - const struct TALER_BANK_CreditDetails *details, - const json_t *json) +static void +process_reply (const struct TALER_BANK_CreditDetails *details, + unsigned int details_length) { - struct WireAccount *wa = cls; enum GNUNET_DB_QueryStatus qs; + bool shard_done; + uint64_t lroff = latest_row_off; - (void) json; - if (NULL == details) + if (0 == details_length) + { + /* Server should have used 204, not 200! */ + GNUNET_break_op (0); + transaction_completed (); + return; + } + hh_returned_data = true; + /* check serial IDs for range constraints */ + for (unsigned int i = 0; i<details_length; i++) { - wa->hh = NULL; - if (TALER_EC_NONE != ec) + const struct TALER_BANK_CreditDetails *cd = &details[i]; + + if (cd->serial_id < lroff) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Error fetching history: ec=%u, http_status=%u\n", - (unsigned int) ec, - http_status); + "Serial ID %llu not monotonic (got %llu before). Failing!\n", + (unsigned long long) cd->serial_id, + (unsigned long long) lroff); + db_plugin->rollback (db_plugin->cls); + GNUNET_SCHEDULER_shutdown (); + return; } - else + if (cd->serial_id > shard_end) { + /* we are *past* the current shard (likely because the serial_id of the + shard_end happens to not exist in the DB). So commit and stop this + iteration! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "History response complete\n"); + "Serial ID %llu past shard end at %llu, ending iteration early!\n", + (unsigned long long) cd->serial_id, + (unsigned long long) shard_end); + details_length = i; + progress = true; + lroff = cd->serial_id - 1; + break; } - if (wa->started_transaction) + lroff = cd->serial_id; + } + if (0 != details_length) + { + enum GNUNET_DB_QueryStatus qss[details_length]; + struct TALER_EXCHANGEDB_ReserveInInfo reserves[details_length]; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Importing %u transactions\n", + details_length); + for (unsigned int i = 0; i<details_length; i++) + { + const struct TALER_BANK_CreditDetails *cd = &details[i]; + struct TALER_EXCHANGEDB_ReserveInInfo *res = &reserves[i]; + + res->reserve_pub = &cd->reserve_pub; + res->balance = &cd->amount; + res->execution_time = cd->execution_date; + res->sender_account_details = cd->debit_account_uri; + res->exchange_account_name = ai->section_name; + res->wire_reference = cd->serial_id; + } + qs = db_plugin->reserves_in_insert (db_plugin->cls, + reserves, + details_length, + qss); + switch (qs) { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "End of list. Committing progress!\n"); - do_commit (wa); + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got DB soft error for reserves_in_insert (%u). Rolling back.\n", + details_length); + handle_soft_error (); + return; + default: + break; } - else + for (unsigned int i = 0; i<details_length; i++) { - if ( (wa->delay) && - (test_mode) && - (NULL == wa->next) ) + const struct TALER_BANK_CreditDetails *cd = &details[i]; + + switch (qss[i]) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Shutdown due to test mode!\n"); + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); - return GNUNET_OK; - } - else - { - shard_completed (wa); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got DB soft error for batch_reserves_in_insert(%u). Rolling back.\n", + i); + handle_soft_error (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Either wirewatch was freshly started after the system was + shutdown and we're going over an incomplete shard again + after being restarted, or the shard lock period was too + short (number of workers set incorrectly?) and a 2nd + wirewatcher has been stealing our work while we are still + at it. */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Attempted to import transaction %llu (%s) twice. " + "This should happen rarely (if not, ask for support).\n", + (unsigned long long) cd->serial_id, + job_name); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Imported transaction %llu.\n", + (unsigned long long) cd->serial_id); + /* normal case */ + progress = true; + break; } } - return GNUNET_OK; /* will be ignored anyway */ } - if (serial_id < wa->latest_row_off) + + latest_row_off = lroff; + shard_done = (shard_end <= latest_row_off); + if (shard_done) { - /* we are done with the current shard, commit and stop this iteration! */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Serial ID %llu not monotonic (got %llu before). Failing!\n", - (unsigned long long) serial_id, - (unsigned long long) wa->latest_row_off); - if (wa->started_transaction) + /* shard is complete, mark this as well */ + qs = db_plugin->complete_shard (db_plugin->cls, + job_name, + shard_start, + shard_end); + switch (qs) { - wa->started_transaction = false; - db_plugin->rollback (db_plugin->cls); + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got DB soft error for complete_shard. Rolling back.\n"); + handle_soft_error (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + /* Not expected, but let's just continue */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* normal case */ + progress = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Completed shard %s (%llu,%llu] after %s\n", + job_name, + (unsigned long long) shard_start, + (unsigned long long) shard_end, + GNUNET_STRINGS_relative_time_to_string ( + GNUNET_TIME_absolute_get_duration (shard_start_time), + true)); + break; } - GNUNET_SCHEDULER_shutdown (); - wa->hh = NULL; - return GNUNET_SYSERR; + shard_delay = GNUNET_TIME_absolute_get_duration (shard_start_time); + shard_open = false; + transaction_completed (); + return; } - if (serial_id > wa->shard_end) + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&continue_with_shard, + NULL); +} + + +/** + * Callbacks of this type are used to serve the result of asking + * the bank for the transaction history. + * + * @param cls NULL + * @param reply response we got from the bank + */ +static void +history_cb (void *cls, + const struct TALER_BANK_CreditHistoryResponse *reply) +{ + (void) cls; + GNUNET_assert (NULL == task); + hh = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "History request returned with HTTP status %u\n", + reply->http_status); + switch (reply->http_status) { - /* we are done with the current shard, commit and stop this iteration! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Serial ID %llu past shard end at %llu, ending iteration early!\n", - (unsigned long long) serial_id, - (unsigned long long) wa->shard_end); - wa->latest_row_off = serial_id - 1; - wa->delay = false; - if (wa->started_transaction) - { - do_commit (wa); - } - else + case MHD_HTTP_OK: + process_reply (reply->details.ok.details, + reply->details.ok.details_length); + return; + case MHD_HTTP_NO_CONTENT: + transaction_completed (); + return; + case MHD_HTTP_NOT_FOUND: + hh_account_404 = true; + if (ignore_account_404) { - if (mark_shard_done (wa)) - shard_completed (wa); + transaction_completed (); + return; } - wa->hh = NULL; - return GNUNET_SYSERR; + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Error fetching history: %s (%u)\n", + TALER_ErrorCode_get_hint (reply->ec), + reply->http_status); + break; } - if (! wa->started_transaction) + if (! exit_on_error) { - if (GNUNET_OK != - db_plugin->start_read_committed (db_plugin->cls, - "wirewatch check for incoming wire transfers")) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to start database transaction!\n"); - global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - wa->hh = NULL; - return GNUNET_SYSERR; - } - wa_pos->shard_start_time = GNUNET_TIME_absolute_get (); - wa->started_transaction = true; + transaction_completed (); + return; } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Adding wire transfer over %s with (hashed) subject `%s'\n", - TALER_amount2s (&details->amount), - TALER_B2S (&details->reserve_pub)); - /* FIXME-PERFORMANCE: Consider using Postgres multi-valued insert here, - for up to 15x speed-up according to - https://dba.stackexchange.com/questions/224989/multi-row-insert-vs-transactional-single-row-inserts#225006 - (Note: this may require changing both the - plugin API as well as modifying how this function is called.) */ - qs = db_plugin->reserves_in_insert (db_plugin->cls, - &details->reserve_pub, - &details->amount, - details->execution_date, - details->debit_account_uri, - wa->ai->section_name, - serial_id); - switch (qs) + GNUNET_SCHEDULER_shutdown (); +} + + +static void +continue_with_shard (void *cls) +{ + unsigned int limit; + + (void) cls; + task = NULL; + GNUNET_assert (shard_end > latest_row_off); + limit = GNUNET_MIN (batch_size, + shard_end - latest_row_off); + GNUNET_assert (NULL == hh); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Requesting credit history starting from %llu\n", + (unsigned long long) latest_row_off); + hh_start_time = GNUNET_TIME_absolute_get (); + hh_returned_data = false; + hh_account_404 = false; + hh = TALER_BANK_credit_history (ctx, + ai->auth, + latest_row_off, + limit, + test_mode + ? GNUNET_TIME_UNIT_ZERO + : longpoll_timeout, + &history_cb, + NULL); + if (NULL == hh) { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - db_plugin->rollback (db_plugin->cls); - wa->started_transaction = false; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start request for account history!\n"); + global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); - wa->hh = NULL; - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got DB soft error for reserves_in_insert. Rolling back.\n"); - handle_soft_error (wa); - wa->hh = NULL; - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* already existed, ok, let's just continue */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* normal case */ - break; + return; } - wa->delay = false; - wa->latest_row_off = serial_id; - return GNUNET_OK; } /** - * Query for incoming wire transfers. + * Reserve a shard for us to work on. * * @param cls NULL */ static void -find_transfers (void *cls) +lock_shard (void *cls) { enum GNUNET_DB_QueryStatus qs; - unsigned int limit; + struct GNUNET_TIME_Relative delay; + uint64_t last_shard_start = shard_start; + uint64_t last_shard_end = shard_end; (void) cls; task = NULL; @@ -680,100 +797,122 @@ find_transfers (void *cls) GNUNET_SCHEDULER_shutdown (); return; } - wa_pos->delay = true; - wa_pos->current_batch_size = 0; /* reset counter */ - if (wa_pos->shard_end <= wa_pos->batch_start) + if ( (shard_open) && + (GNUNET_TIME_absolute_is_future (shard_end_time)) ) { - uint64_t start; - uint64_t end; - struct GNUNET_TIME_Relative delay; - /* advance to next shard */ - - if (0 == max_workers) - delay = GNUNET_TIME_UNIT_ZERO; - else - delay.rel_value_us = GNUNET_CRYPTO_random_u64 ( - GNUNET_CRYPTO_QUALITY_WEAK, - 4 * GNUNET_TIME_relative_max ( - wirewatch_idle_sleep_interval, - GNUNET_TIME_relative_multiply (shard_delay, - max_workers)).rel_value_us); - qs = db_plugin->begin_shard (db_plugin->cls, - wa_pos->job_name, - delay, - shard_size, - &start, - &end); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to obtain starting point for montoring from database!\n"); - global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - /* try again */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Serialization error tying to obtain shard, will try again in %s!\n", - GNUNET_STRINGS_relative_time_to_string ( - wirewatch_idle_sleep_interval, - GNUNET_YES)); - task = GNUNET_SCHEDULER_add_delayed (wirewatch_idle_sleep_interval, - &find_transfers, - NULL); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No shard available, will try again in %s!\n", - GNUNET_STRINGS_relative_time_to_string ( - wirewatch_idle_sleep_interval, - GNUNET_YES)); - task = GNUNET_SCHEDULER_add_delayed (wirewatch_idle_sleep_interval, - &find_transfers, - NULL); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - wa_pos->shard_start = start; - wa_pos->shard_end = end; - wa_pos->batch_start = start; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting with shard at [%llu,%llu) locked for %s\n", - (unsigned long long) start, - (unsigned long long) end, - GNUNET_STRINGS_relative_time_to_string (delay, - GNUNET_YES)); - break; - } + progress = false; + batch_start = latest_row_off; + task = GNUNET_SCHEDULER_add_now (&continue_with_shard, + NULL); + return; } - - limit = GNUNET_MIN (wa_pos->batch_size, - wa_pos->shard_end - wa_pos->batch_start); - GNUNET_assert (NULL == wa_pos->hh); - wa_pos->latest_row_off = wa_pos->batch_start; - wa_pos->hh = TALER_BANK_credit_history (ctx, - wa_pos->ai->auth, - wa_pos->batch_start, - limit, - test_mode - ? GNUNET_TIME_UNIT_ZERO - : LONGPOLL_TIMEOUT, - &history_cb, - wa_pos); - if (NULL == wa_pos->hh) + if (shard_open) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Shard not completed in time, will try to re-acquire\n"); + /* How long we lock a shard depends on the number of + workers expected, and how long we usually took to + process a shard. */ + if (0 == max_workers) + delay = GNUNET_TIME_UNIT_ZERO; + else + delay.rel_value_us = GNUNET_CRYPTO_random_u64 ( + GNUNET_CRYPTO_QUALITY_WEAK, + 4 * GNUNET_TIME_relative_max ( + wirewatch_idle_sleep_interval, + GNUNET_TIME_relative_multiply (shard_delay, + max_workers)).rel_value_us); + shard_start_time = GNUNET_TIME_absolute_get (); + qs = db_plugin->begin_shard (db_plugin->cls, + job_name, + delay, + shard_size, + &shard_start, + &shard_end); + switch (qs) { + case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to start request for account history!\n"); - if (wa_pos->started_transaction) - { - db_plugin->rollback (db_plugin->cls); - wa_pos->started_transaction = false; - } + "Failed to obtain starting point for montoring from database!\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* try again */ + { + struct GNUNET_TIME_Relative rdelay; + + wirewatch_conflict_sleep_interval + = GNUNET_TIME_STD_BACKOFF (wirewatch_conflict_sleep_interval); + rdelay = GNUNET_TIME_randomize (wirewatch_conflict_sleep_interval); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Serialization error tying to obtain shard %s, will try again in %s!\n", + job_name, + GNUNET_STRINGS_relative_time_to_string (rdelay, + true)); +#if 1 + if (GNUNET_TIME_relative_cmp (rdelay, + >, + GNUNET_TIME_UNIT_SECONDS)) + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Delay would have been for %s\n", + GNUNET_TIME_relative2s (rdelay, + true)); + rdelay = GNUNET_TIME_relative_min (rdelay, + GNUNET_TIME_UNIT_SECONDS); +#endif + delayed_until = GNUNET_TIME_relative_to_absolute (rdelay); + } + GNUNET_assert (NULL == task); + schedule_transfers (); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No shard available, will try again for %s in %s!\n", + job_name, + GNUNET_STRINGS_relative_time_to_string ( + wirewatch_idle_sleep_interval, + true)); + delayed_until = GNUNET_TIME_relative_to_absolute ( + wirewatch_idle_sleep_interval); + shard_open = false; + GNUNET_assert (NULL == task); + schedule_transfers (); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* continued below */ + wirewatch_conflict_sleep_interval = GNUNET_TIME_UNIT_ZERO; + break; + } + shard_end_time = GNUNET_TIME_relative_to_absolute (delay); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting with shard %s at (%llu,%llu] locked for %s\n", + job_name, + (unsigned long long) shard_start, + (unsigned long long) shard_end, + GNUNET_STRINGS_relative_time_to_string (delay, + true)); + progress = false; + batch_start = shard_start; + if ( (shard_open) && + (shard_start == last_shard_start) && + (shard_end == last_shard_end) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Continuing from %llu\n", + (unsigned long long) latest_row_off); + GNUNET_break (latest_row_off >= batch_start); /* resume where we left things */ } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resetting shard start to original start point (%d)\n", + shard_open ? 1 : 0); + latest_row_off = batch_start; + } + shard_open = true; + task = GNUNET_SCHEDULER_add_now (&continue_with_shard, + NULL); } @@ -796,27 +935,26 @@ run (void *cls, (void) cfgfile; cfg = c; + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + cls); if (GNUNET_OK != exchange_serve_process_config ()) { global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } - wa_pos = wa_head; - GNUNET_assert (NULL != wa_pos); - GNUNET_SCHEDULER_add_shutdown (&shutdown_task, - cls); ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc); - rc = GNUNET_CURL_gnunet_rc_create (ctx); if (NULL == ctx) { GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_NO_RESTART; return; } - - task = GNUNET_SCHEDULER_add_now (&find_transfers, - NULL); + rc = GNUNET_CURL_gnunet_rc_create (ctx); + schedule_transfers (); } @@ -832,6 +970,24 @@ main (int argc, char *const *argv) { struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_string ('a', + "account", + "SECTION_NAME", + "name of the configuration section with the account we should watch (needed if more than one is enabled for crediting)", + &account_section), + GNUNET_GETOPT_option_flag ('e', + "exit-on-error", + "terminate wirewatch if we failed to download information from the bank", + &exit_on_error), + GNUNET_GETOPT_option_relative_time ('f', + "longpoll-timeout", + "DELAY", + "what is the timeout when asking the bank about new transactions, specify with unit (e.g. --longpoll-timeout=30s)", + &longpoll_timeout), + GNUNET_GETOPT_option_flag ('I', + "ignore-not-found", + "continue, even if the bank account of the exchange was not found", + &ignore_account_404), GNUNET_GETOPT_option_uint ('S', "size", "SIZE", @@ -853,6 +1009,7 @@ main (int argc, }; enum GNUNET_GenericReturnValue ret; + longpoll_timeout = LONGPOLL_TIMEOUT; if (GNUNET_OK != GNUNET_STRINGS_get_utf8_args (argc, argv, &argc, &argv)) diff --git a/src/exchange/test_taler_exchange_httpd.conf b/src/exchange/test_taler_exchange_httpd.conf index 2adee5053..7e7ff8b45 100644 --- a/src/exchange/test_taler_exchange_httpd.conf +++ b/src/exchange/test_taler_exchange_httpd.conf @@ -13,6 +13,8 @@ TINY_AMOUNT = EUR:0.01 [exchange] +AML_THRESHOLD = EUR:1000000 + # Directory with our terms of service. TERMS_DIR = ../../contrib/tos @@ -67,10 +69,10 @@ ENABLE_CREDIT = YES WIRE_GATEWAY_AUTH_METHOD = basic USERNAME = Exchange PASSWORD = x -WIRE_GATEWAY_URL = "http://localhost:8082/3/" +WIRE_GATEWAY_URL = "http://localhost:8082/accounts/3/taler-wire-gateway/" # Coins for the tests. -[coin_eur_ct_1] +[coin_eur_ct_1_rsa] value = EUR:0.01 duration_withdraw = 7 days duration_spend = 2 years @@ -79,9 +81,21 @@ fee_withdraw = EUR:0.00 fee_deposit = EUR:0.00 fee_refresh = EUR:0.01 fee_refund = EUR:0.01 +CIPHER = RSA rsa_keysize = 1024 -[coin_eur_ct_10] +[coin_eur_ct_1_cs] +value = EUR:0.01 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.00 +fee_deposit = EUR:0.00 +fee_refresh = EUR:0.01 +fee_refund = EUR:0.01 +CIPHER = CS + +[coin_eur_ct_10_rsa] value = EUR:0.10 duration_withdraw = 7 days duration_spend = 2 years @@ -90,9 +104,21 @@ fee_withdraw = EUR:0.01 fee_deposit = EUR:0.01 fee_refresh = EUR:0.03 fee_refund = EUR:0.01 +CIPHER = RSA rsa_keysize = 1024 -[coin_eur_1] +[coin_eur_ct_10_cs] +value = EUR:0.10 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +fee_refund = EUR:0.01 +CIPHER = CS + +[coin_eur_1_rsa] value = EUR:1 duration_withdraw = 7 days duration_spend = 2 years @@ -101,4 +127,16 @@ fee_withdraw = EUR:0.01 fee_deposit = EUR:0.01 fee_refresh = EUR:0.03 fee_refund = EUR:0.01 +CIPHER = RSA rsa_keysize = 1024 + +[coin_eur_1_cs] +value = EUR:1 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +fee_refund = EUR:0.01 +CIPHER = CS diff --git a/src/exchange/test_taler_exchange_httpd.sh b/src/exchange/test_taler_exchange_httpd.sh index e8dc46af8..0fe71f3ad 100755 --- a/src/exchange/test_taler_exchange_httpd.sh +++ b/src/exchange/test_taler_exchange_httpd.sh @@ -1,7 +1,7 @@ #!/bin/bash # # This file is part of TALER -# Copyright (C) 2015-2020 Taler Systems SA +# Copyright (C) 2015-2020, 2023 Taler Systems SA # # TALER is free software; you can redistribute it and/or modify it under the # terms of the GNU Affero General Public License as published by the Free Software @@ -23,6 +23,8 @@ # Clear environment from variables that override config. unset XDG_DATA_HOME unset XDG_CONFIG_HOME + +set -eu # echo -n "Launching exchange ..." PREFIX= @@ -30,7 +32,7 @@ PREFIX= #PREFIX="valgrind --leak-check=yes --track-fds=yes --error-exitcode=1 --log-file=valgrind.%p" # Setup database -taler-exchange-dbinit -c test_taler_exchange_httpd.conf &> /dev/null +taler-exchange-dbinit -c test_taler_exchange_httpd.conf &> /dev/null || exit 77 # Run Exchange HTTPD (in background) $PREFIX taler-exchange-httpd -c test_taler_exchange_httpd.conf 2> test-exchange.log & diff --git a/src/exchange/test_taler_exchange_unix.conf b/src/exchange/test_taler_exchange_unix.conf index b9387f603..e96bfba3f 100644 --- a/src/exchange/test_taler_exchange_unix.conf +++ b/src/exchange/test_taler_exchange_unix.conf @@ -70,7 +70,7 @@ TALER_BANK_AUTH_METHOD = NONE # Coins for the tests. -[coin_eur_ct_1] +[coin_eur_ct_1_rsa] value = EUR:0.01 duration_withdraw = 7 days duration_spend = 2 years @@ -79,9 +79,21 @@ fee_withdraw = EUR:0.00 fee_deposit = EUR:0.00 fee_refresh = EUR:0.01 fee_refund = EUR:0.01 +CIPHER = RSA rsa_keysize = 1024 -[coin_eur_ct_10] +[coin_eur_ct_1_cs] +value = EUR:0.01 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.00 +fee_deposit = EUR:0.00 +fee_refresh = EUR:0.01 +fee_refund = EUR:0.01 +CIPHER = CS + +[coin_eur_ct_10_rsa] value = EUR:0.10 duration_withdraw = 7 days duration_spend = 2 years @@ -90,9 +102,21 @@ fee_withdraw = EUR:0.01 fee_deposit = EUR:0.01 fee_refresh = EUR:0.03 fee_refund = EUR:0.01 +CIPHER = RSA rsa_keysize = 1024 -[coin_eur_1] +[coin_eur_ct_10_cs] +value = EUR:0.10 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +fee_refund = EUR:0.01 +CIPHER = CS + +[coin_eur_1_rsa] value = EUR:1 duration_withdraw = 7 days duration_spend = 2 years @@ -101,4 +125,16 @@ fee_withdraw = EUR:0.01 fee_deposit = EUR:0.01 fee_refresh = EUR:0.03 fee_refund = EUR:0.01 +CIPHER = RSA rsa_keysize = 1024 + +[coin_eur_1_cs] +value = EUR:1 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +fee_refund = EUR:0.01 +CIPHER = CS |