diff options
Diffstat (limited to 'src/exchange')
79 files changed, 7352 insertions, 6571 deletions
diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index 3d87a2a58..1c0c2c684 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -15,6 +15,8 @@ pkgcfg_DATA = \ exchange.conf # Programs +bin_SCRIPTS = \ + taler-exchange-kyc-aml-pep-trigger.sh bin_PROGRAMS = \ taler-exchange-aggregator \ @@ -122,20 +124,21 @@ taler_exchange_wirewatch_LDADD = \ 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_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_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 \ @@ -177,12 +180,10 @@ taler_exchange_httpd_SOURCES = \ 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_reserves_status.c taler-exchange-httpd_reserves_status.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) \ @@ -214,7 +215,6 @@ check_SCRIPTS += \ test_taler_exchange_httpd_afl.sh endif -.NOTPARALLEL: TESTS = \ $(check_SCRIPTS) @@ -227,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 750e71724..ce471a292 100644 --- a/src/exchange/exchange.conf +++ b/src/exchange/exchange.conf @@ -6,10 +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. @@ -19,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 @@ -44,10 +67,6 @@ 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 @@ -66,7 +85,7 @@ ROUTER_IDLE_SLEEP_INTERVAL = 60 s # 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 = 1 h +EXPIRE_SHARD_SIZE = 60 s # How long should the transfer tool # sleep if it has nothing to do? @@ -107,13 +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 = tos-v0 +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 = pp-v0 +PRIVACY_ETAG = exchange-pp-v0 diff --git a/src/exchange/taler-exchange-aggregator.c b/src/exchange/taler-exchange-aggregator.c index 38110a5e7..691d65ae3 100644 --- a/src/exchange/taler-exchange-aggregator.c +++ b/src/exchange/taler-exchange-aggregator.c @@ -28,6 +28,7 @@ #include "taler_json_lib.h" #include "taler_kyclogic_lib.h" #include "taler_bank_service.h" +#include "taler_dbevents.h" /** @@ -303,17 +304,17 @@ parse_aggregator_config (void) (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"); + "Need non-zero amount in section `taler' under `CURRENCY_ROUND_UNIT'\n"); return GNUNET_SYSERR; } if (GNUNET_OK != TALER_config_get_amount (cfg, - "taler", + "exchange", "AML_THRESHOLD", &aml_threshold)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Need amount in section `TALER' under `AML_THRESHOLD'\n"); + "Need amount in section `exchange' under `AML_THRESHOLD'\n"); return GNUNET_SYSERR; } @@ -382,6 +383,7 @@ release_shard (struct Shard *s) 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: @@ -499,6 +501,8 @@ 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, @@ -522,6 +526,7 @@ kyc_satisfied (struct AggregationUnit *au_active) db_plugin->cls, requirement, &au_active->h_payto, + NULL, /* not a reserve */ &au_active->requirement_row); if (qs < 0) { @@ -723,10 +728,16 @@ do_aggregate (struct AggregationUnit *au) 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; } qs = db_plugin->aggregate (db_plugin->cls, @@ -748,7 +759,7 @@ do_aggregate (struct AggregationUnit *au) "Serialization issue, trying again later!\n"); return GNUNET_NO; } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + 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 @@ -814,15 +825,30 @@ do_aggregate (struct AggregationUnit *au) { case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Serialization issue during aggregation; trying again later!\n"); + "Serialization issue during aggregation; trying again later!\n") + ; return GNUNET_NO; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); global_ret = EXIT_FAILURE; return GNUNET_SYSERR; default: - return GNUNET_OK; + 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; + } @@ -896,18 +922,28 @@ 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, &drain_kyc_alerts, NULL); + } else + { task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, NULL); + } return; } case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: @@ -925,6 +961,7 @@ run_aggregation (void *cls) switch (ret) { case GNUNET_SYSERR: + global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); db_plugin->rollback (db_plugin->cls); release_shard (s); @@ -1021,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); @@ -1038,6 +1076,7 @@ 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); } @@ -1188,6 +1227,7 @@ drain_kyc_alerts (void *cls) { case GNUNET_SYSERR: GNUNET_break (0); + global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); db_plugin->rollback (db_plugin->cls); /* just in case */ return; diff --git a/src/exchange/taler-exchange-closer.c b/src/exchange/taler-exchange-closer.c index 41c6436a1..779525c4e 100644 --- a/src/exchange/taler-exchange-closer.c +++ b/src/exchange/taler-exchange-closer.c @@ -312,10 +312,10 @@ expired_reserve_cb (void *cls, memset (&wtid, 0, sizeof (wtid)); - memcpy (&wtid, - reserve_pub, - GNUNET_MIN (sizeof (wtid), - sizeof (*reserve_pub))); + GNUNET_memcpy (&wtid, + reserve_pub, + GNUNET_MIN (sizeof (wtid), + sizeof (*reserve_pub))); qs = db_plugin->insert_reserve_closed (db_plugin->cls, reserve_pub, now, @@ -469,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-expire.c b/src/exchange/taler-exchange-expire.c index d99c430e0..b2d34ee1c 100644 --- a/src/exchange/taler-exchange-expire.c +++ b/src/exchange/taler-exchange-expire.c @@ -74,11 +74,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? - */ -static struct GNUNET_TIME_Relative expire_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. @@ -141,17 +136,6 @@ shutdown_task (void *cls) static enum GNUNET_GenericReturnValue parse_expire_config (void) { - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (cfg, - "exchange", - "EXPIRE_IDLE_SLEEP_INTERVAL", - &expire_idle_sleep_interval)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange", - "EXPIRE_IDLE_SLEEP_INTERVAL"); - return GNUNET_SYSERR; - } if (NULL == (db_plugin = TALER_EXCHANGEDB_plugin_load (cfg))) { @@ -223,7 +207,12 @@ release_shard (struct Shard *s) if ( (0 == wc) && (test_mode) && (! jump_mode) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "In test-mode without work. Terminating.\n"); GNUNET_SCHEDULER_shutdown (); + return; + } } @@ -283,9 +272,9 @@ run_expire (void *cls) "expire-purse")) { GNUNET_break (0); - global_ret = EXIT_FAILURE; db_plugin->rollback (db_plugin->cls); abort_shard (s); + global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } @@ -296,9 +285,9 @@ run_expire (void *cls) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - global_ret = EXIT_FAILURE; db_plugin->rollback (db_plugin->cls); abort_shard (s); + global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SOFT_ERROR: @@ -395,6 +384,13 @@ run_shard (void *cls) 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, diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index d247d981b..36459fbd7 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.c @@ -30,20 +30,23 @@ #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_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_deposit.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" @@ -65,11 +68,9 @@ #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_reserves_status.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" @@ -149,11 +150,41 @@ struct TALER_AttributeEncryptionKeyP TEH_attribute_key; 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? @@ -222,6 +253,22 @@ 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; @@ -293,10 +340,6 @@ handle_post_coins (struct TEH_RequestContext *rc, } h[] = { { - .op = "deposit", - .handler = &TEH_handler_deposit - }, - { .op = "melt", .handler = &TEH_handler_melt }, @@ -342,6 +385,57 @@ 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. * @@ -542,7 +636,6 @@ handle_get_aml (struct TEH_RequestContext *rc, TALER_EC_GENERIC_DB_FETCH_FAILED, NULL); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED, @@ -563,6 +656,46 @@ handle_get_aml (struct TEH_RequestContext *rc, /** + * 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 @@ -609,16 +742,8 @@ handle_post_reserves (struct TEH_RequestContext *rc, .handler = &TEH_handler_batch_withdraw }, { - .op = "withdraw", - .handler = &TEH_handler_withdraw - }, - { - .op = "status", - .handler = &TEH_handler_reserves_status - }, - { - .op = "history", - .handler = &TEH_handler_reserves_history + .op = "age-withdraw", + .handler = &TEH_handler_age_withdraw }, { .op = "purse", @@ -662,6 +787,87 @@ handle_post_reserves (struct TEH_RequestContext *rc, /** + * 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 @@ -815,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 @@ -881,7 +1092,6 @@ proceed_with_handler (struct TEH_RequestContext *rc, const struct TEH_RequestHandler *rh = rc->rh; 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 @@ -902,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; @@ -911,16 +1122,16 @@ 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 */ } } @@ -932,9 +1143,9 @@ proceed_with_handler (struct TEH_RequestContext *rc, /* Parse command-line arguments */ /* make a copy of 'url' because 'strtok_r()' will modify */ - memcpy (d, - url, - ulen); + GNUNET_memcpy (d, + url, + ulen); i = 0; args[i++] = strtok_r (d, "/", &sp); while ( (NULL != args[i - 1]) && @@ -957,7 +1168,6 @@ proceed_with_handler (struct TEH_RequestContext *rc, 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, @@ -970,7 +1180,7 @@ proceed_with_handler (struct TEH_RequestContext *rc, if (0 == strcasecmp (rh->method, MHD_HTTP_METHOD_POST)) ret = rh->handler.post (rc, - root, + rc->root, args); else if (0 == strcasecmp (rh->method, MHD_HTTP_METHOD_DELETE)) @@ -980,7 +1190,6 @@ proceed_with_handler (struct TEH_RequestContext *rc, ret = rh->handler.get (rc, args); } - json_decref (root); return ret; } @@ -1023,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 @@ -1035,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); @@ -1130,108 +1402,22 @@ handle_post_management (struct TEH_RequestContext *rc, &exchange_pub, root); } - /* FIXME-STYLE: all of the following can likely be nicely combined - into an array-based dispatcher to deduplicate the logic... */ - if (0 == strcmp (args[0], - "keys")) - { - if (NULL != args[1]) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/keys/*"); - } - 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/*"); + for (unsigned int i = 0; + NULL != plain_posts[i].handler; + i++) + { + if (0 == strcmp (args[0], + plain_posts[i].arg0)) + { + 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_wire_fees (rc->connection, - root); - } - if (0 == strcmp (args[0], - "global-fee")) - { - if (NULL != args[1]) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/global-fee/*"); - } - return TEH_handler_management_post_global_fees (rc->connection, - root); - } - if (0 == strcmp (args[0], - "extensions")) - { - if (NULL != args[1]) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/extensions/*"); - } - return TEH_handler_management_post_extensions (rc->connection, - root); - } - if (0 == strcmp (args[0], - "drain")) - { - if (NULL != args[1]) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/drain/*"); - } - return TEH_handler_management_post_drain (rc->connection, - root); - } - if (0 == strcmp (args[0], - "aml-officers")) - { - if (NULL != args[1]) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/aml-officers/*"); - } - return TEH_handler_management_aml_officers (rc->connection, - root); - } - if (0 == strcmp (args[0], - "partners")) - { - if (NULL != args[1]) - { - GNUNET_break_op (0); - return r404 (rc->connection, - "/management/partners/*"); - } - return TEH_handler_management_partners (rc->connection, - root); } GNUNET_break_op (0); return r404 (rc->connection, @@ -1321,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) @@ -1353,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 @@ -1407,12 +1639,6 @@ 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, @@ -1436,8 +1662,9 @@ handle_mhd_request (void *cls, { .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", @@ -1446,6 +1673,12 @@ handle_mhd_request (void *cls, .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, @@ -1467,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 */ { @@ -1538,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", @@ -1575,7 +1823,13 @@ handle_mhd_request (void *cls, .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 */ { @@ -1617,33 +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) - { - GNUNET_break_op (0); - return TALER_MHD_reply_request_too_large (connection); - } - } + TALER_MHD_check_content_length (connection, + TALER_MHD_REQUEST_BUFFER_MAX); } } @@ -1830,6 +2059,11 @@ handle_mhd_request (void *cls, static enum GNUNET_GenericReturnValue 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 != TALER_KYCLOGIC_kyc_init (TEH_cfg)) { @@ -1871,6 +2105,25 @@ exchange_serve_process_config (void) return GNUNET_SYSERR; } if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (TEH_cfg, + "exchange", + "KYC_AML_TRIGGER", + &TEH_kyc_aml_trigger)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "KYC_AML_TRIGGER"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (TEH_cfg, + "exchange", + "TOPLEVEL_REDIRECT_URL", + &toplevel_redirect_url)) + { + toplevel_redirect_url = GNUNET_strdup ("/terms"); + } + if (GNUNET_OK != TALER_config_get_currency (TEH_cfg, &TEH_currency)) { @@ -1879,21 +2132,97 @@ exchange_serve_process_config (void) "CURRENCY"); return GNUNET_SYSERR; } + if (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, + TALER_CONFIG_parse_currencies (TEH_cfg, + &num_cspecs, + &cspecs)) + return GNUNET_SYSERR; + for (unsigned int i = 0; i<num_cspecs; i++) + { + struct TALER_CurrencySpecification *cspec; + + cspec = &cspecs[i]; + if (0 == strcmp (TEH_currency, + cspec->currency)) + { + TEH_cspec = cspec; + break; + } + } + if (NULL == TEH_cspec) + { + 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 != + TALER_config_get_amount (TEH_cfg, + "exchange", "AML_THRESHOLD", &TEH_aml_threshold)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Need amount in section `TALER' under `AML_THRESHOLD'\n"); + "Need amount in section `exchange' under `AML_THRESHOLD'\n"); return GNUNET_SYSERR; } + if (GNUNET_OK != + TALER_config_get_amount (TEH_cfg, + "exchange", + "STEFAN_ABS", + &TEH_stefan_abs)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &TEH_stefan_abs)); + } + if (GNUNET_OK != + TALER_config_get_amount (TEH_cfg, + "exchange", + "STEFAN_LOG", + &TEH_stefan_log)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &TEH_stefan_log)); + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_float (TEH_cfg, + "exchange", + "STEFAN_LIN", + &TEH_stefan_lin)) + { + TEH_stefan_lin = 0.0f; + } + if (0 != strcmp (TEH_currency, TEH_aml_threshold.currency)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Amount in section `TALER' under `AML_THRESHOLD' uses the wrong currency!\n"); + "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 != @@ -1931,11 +2260,10 @@ 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_config_invalid (GNUNET_ERROR_TYPE_ERROR, "exchange", @@ -2128,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; } @@ -2215,6 +2545,7 @@ 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 (); @@ -2244,6 +2575,11 @@ do_shutdown (void *cls) exchange_curl_rc = NULL; } TALER_TEMPLATING_done (); + TEH_cspec = NULL; + TALER_CONFIG_free_currencies (num_cspecs, + cspecs); + num_cspecs = 0; + cspecs = NULL; } @@ -2282,37 +2618,47 @@ run (void *cls, 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_FAILURE; + 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; } @@ -2324,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; } @@ -2377,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 5ab0ea92b..25e9e1105 100644 --- a/src/exchange/taler-exchange-httpd.h +++ b/src/exchange/taler-exchange-httpd.h @@ -65,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; @@ -93,11 +98,36 @@ extern struct TALER_AttributeEncryptionKeyP TEH_attribute_key; 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? @@ -169,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. diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.c b/src/exchange/taler-exchange-httpd_age-withdraw.c index 170cd06a5..9276fb191 100644 --- a/src/exchange/taler-exchange-httpd_age-withdraw.c +++ b/src/exchange/taler-exchange-httpd_age-withdraw.c @@ -22,14 +22,516 @@ * @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. @@ -72,46 +574,62 @@ reply_age_withdraw_success ( /** - * Context for #age_withdraw_transaction. + * 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 */ -struct AgeWithdrawContext +static bool +request_is_idempotent (struct MHD_Connection *con, + struct AgeWithdrawContext *awc, + MHD_RESULT *mret) { - /** - * KYC status for the operation. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; + enum GNUNET_DB_QueryStatus qs; + struct TALER_EXCHANGEDB_AgeWithdraw commitment; - /** - * Hash of the wire source URL, needed when kyc is needed. - */ - struct TALER_PaytoHashP h_payto; + 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. */ + } - /** - * The data from the age-withdraw request - */ - struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment; + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return false; - /** - * Current time for the DB transaction. - */ - struct GNUNET_TIME_Timestamp now; -}; + /* 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 + * 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 + * @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, @@ -151,9 +669,6 @@ age_withdraw_amount_cb (void *cls, * IF it returns the soft error code, the function MAY be called again * to retry and MUST not queue a MHD response. * - * Note that "awc->commitment.sig" is set before entering this function as we - * signed before entering the transaction. - * * @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, @@ -167,20 +682,17 @@ age_withdraw_transaction (void *cls, { struct AgeWithdrawContext *awc = cls; enum GNUNET_DB_QueryStatus qs; - bool found = false; - bool balance_ok = false; - uint64_t ruuid; - awc->now = GNUNET_TIME_timestamp_get (); 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, + /* 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; @@ -199,59 +711,104 @@ age_withdraw_transaction (void *cls, 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, - "kyc_test_required"); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "kyc_test_required"); } return qs; } if (NULL != kyc_required) { - /* insert KYC requirement into DB! */ + /* 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; - qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls, - &awc->commitment, - awc->now, - &found, - &balance_ok, - &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_age_withdraw"); - return qs; - } - else 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; - } - else if (! balance_ok) + + /* KYC requirement fulfilled, do the age-withdraw transaction */ { - TEH_plugin->rollback (TEH_plugin->cls); - *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( - connection, - TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS, - &awc->commitment.amount_with_fee, - &awc->commitment.reserve_pub); - return GNUNET_DB_STATUS_HARD_ERROR; + 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) @@ -261,48 +818,95 @@ age_withdraw_transaction (void *cls, /** - * Check if the @a rc is replayed and we already have an - * answer. If so, replay the existing answer and return the - * HTTP response. + * @brief Sign the chosen blinded coins, debit the reserve and persist + * the commitment. * - * @param rc request context - * @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 + * 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 bool -request_is_idempotent (struct TEH_RequestContext *rc, - struct AgeWithdrawContext *awc, - MHD_RESULT *mret) +static enum GNUNET_GenericReturnValue +sign_and_do_age_withdraw ( + struct MHD_Connection *connection, + struct AgeWithdrawContext *awc, + MHD_RESULT *result) { - enum GNUNET_DB_QueryStatus qs; - struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment; + 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; - qs = TEH_plugin->get_age_withdraw_info (TEH_plugin->cls, - &awc->commitment.reserve_pub, - &awc->commitment.h_commitment, - &commitment); - if (0 > qs) + 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 */ { - 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_age_withdraw_info"); - return true; /* well, kind-of */ + 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; + } } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return false; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Signatures ready, starting DB interaction\n"); - /* generate idempotent reply */ - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++; - *mret = reply_age_withdraw_success (rc->connection, - &commitment.h_commitment, - commitment.noreveal_index); - return true; + /* 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; } @@ -312,21 +916,21 @@ TEH_handler_age_withdraw (struct TEH_RequestContext *rc, const json_t *root) { MHD_RESULT mhd_ret; - struct AgeWithdrawContext awc; + 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_fixed_auto ("h_commitment", - &awc.commitment.h_commitment), - TALER_JSON_spec_amount ("amount", - TEH_currency, - &awc.commitment.amount_with_fee), - GNUNET_JSON_spec_uint32 ("max_age", - &awc.commitment.max_age), GNUNET_JSON_spec_end () }; - memset (&awc, 0, sizeof (awc)); awc.commitment.reserve_pub = *reserve_pub; @@ -342,53 +946,71 @@ TEH_handler_age_withdraw (struct TEH_RequestContext *rc, } do { - /* If request was made before successfully, return the previous answer */ - if (request_is_idempotent (rc, - &awc, - &mhd_ret)) + /* 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; - /* Verify the signature of the request body with the reserve key */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + /* Ensure validity of denoms and calculate amounts and fees */ if (GNUNET_OK != - TALER_wallet_age_withdraw_verify (&awc.commitment.h_commitment, - &awc.commitment.amount_with_fee, - awc.commitment.max_age, - &awc.commitment.reserve_pub, - &awc.commitment.reserve_sig)) - { - GNUNET_break_op (0); - mhd_ret = TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, - NULL); + 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; - } - /* Run the transaction */ + /* Now that amount_with_fee is calculated, verify the signature of + * the request body with the reserve key. + */ if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "run age withdraw", - TEH_MT_REQUEST_AGE_WITHDRAW, - &mhd_ret, - &age_withdraw_transaction, - &awc)) + verify_reserve_signature (rc->connection, + &awc.commitment, + &mhd_ret)) break; - /* Clean up and send back final response */ - GNUNET_JSON_parse_free (spec); + /* 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) - return TEH_RESPONSE_reply_kyc_required (rc->connection, - &awc.h_payto, - &awc.kyc); + 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); - return reply_age_withdraw_success (rc->connection, - &awc.commitment.h_commitment, - awc.commitment.noreveal_index); - } while(0); + } while (0); GNUNET_JSON_parse_free (spec); + free_age_withdraw_context_resources (&awc); return mhd_ret; } diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c index 50d524a2f..c9aca8e99 100644 --- a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c +++ b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c @@ -19,9 +19,13 @@ * @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" @@ -48,60 +52,28 @@ struct AgeRevealContext struct TALER_ReservePublicKeyP reserve_pub; /** - * Number of coins/denonations in the reveal + * Number of coins to reveal. MUST be equal to + * @e num_secrets/(kappa -1). */ uint32_t num_coins; /** - * #num_coins hashes of the denominations from which the coins are withdrawn. - * Those must support age restriction. + * Number of secrets in the reveal. MUST be a multiple of (kappa-1). */ - struct TALER_DenominationHashP *denoms_h; + uint32_t num_secrets; /** - * #num_coins denomination keys, found in the system, according to denoms_h; - */ - struct TEH_DenominationKey *denom_keys; - - /** - * Total sum of all denominations' values - **/ - struct TALER_Amount total_amount; - - /** - * Total sum of all denominations' fees - */ - struct TALER_Amount total_fee; - - /** - * #num_coins hashes of blinded coins. - */ - struct TALER_BlindedCoinHashP *coin_evs; - - /** - * secrets for #num_coins*(kappa - 1) disclosed coins. + * @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. + * the DB via @a ach and @a reserve_pub. */ - struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment; + struct TALER_EXCHANGEDB_AgeWithdraw commitment; }; -/** - * Helper function to free resources in the context - */ -void -age_reveal_context_free (struct AgeRevealContext *actx) -{ - GNUNET_free (actx->denoms_h); - GNUNET_free (actx->denom_keys); - GNUNET_free (actx->coin_evs); - GNUNET_free (actx->disclosed_coin_secrets); -} - /** * Parse the json body of an '/age-withdraw/$ACH/reveal' request. It extracts @@ -109,8 +81,6 @@ age_reveal_context_free (struct AgeRevealContext *actx) * memory for those. * * @param connection The MHD connection to handle - * @param j_denoms_h Array of hashes of the denominations for the withdrawal, in JSON format - * @param j_coin_evs The blinded envelopes in JSON format for the coins that are not revealed and will be signed on success * @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 @@ -119,140 +89,95 @@ age_reveal_context_free (struct AgeRevealContext *actx) static enum GNUNET_GenericReturnValue parse_age_withdraw_reveal_json ( struct MHD_Connection *connection, - const json_t *j_denoms_h, - const json_t *j_coin_evs, 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; - actx->num_coins = json_array_size (j_denoms_h); /* 0, if j_denoms_h is not an array */ + num_entries = json_array_size (j_disclosed_coin_secrets); /* 0, if not an array */ - if (! json_is_array (j_denoms_h)) - error = "denoms_h must be an array"; - else if (! json_is_array (j_coin_evs)) - error = "coin_evs must be an array"; - else if (! json_is_array (j_disclosed_coin_secrets)) + if (! json_is_array (j_disclosed_coin_secrets)) error = "disclosed_coin_secrets must be an array"; - else if (actx->num_coins == 0) - error = "denoms_h must not be empty"; - else if (actx->num_coins != json_array_size (j_coin_evs)) - error = "denoms_h and coins_evs must be arrays of the same size"; - else if (actx->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! - **/ + 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"; - else if (actx->num_coins * (TALER_CNC_KAPPA - 1) - != json_array_size (j_disclosed_coin_secrets)) - error = "the size of array disclosed_coin_secrets must be " - TALER_CNC_KAPPA_MINUS_ONE_STR " times the size of denoms_h"; if (NULL != error) { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - 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 denomination keys */ - actx->denoms_h = GNUNET_new_array (actx->num_coins, - struct TALER_DenominationHashP); - - json_array_foreach (j_denoms_h, idx, value) { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, &actx->denoms_h[idx]), - GNUNET_JSON_spec_end () - }; + /* Parse diclosed keys */ + actx->disclosed_coin_secrets = + GNUNET_new_array (actx->num_secrets, + struct TALER_PlanchetMasterSecretP); - if (GNUNET_OK != - GNUNET_JSON_parse (value, spec, NULL, NULL)) + 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 denoms_h", + "couldn't parse entry no. %d in array disclosed_coin_secrets", idx + 1); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + msg); goto EXIT; - } - }; - - /* Parse blinded envelopes */ - actx->coin_evs = GNUNET_new_array (actx->num_coins, - struct TALER_BlindedCoinHashP); - - json_array_foreach (j_coin_evs, idx, value) { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, &actx->coin_evs[idx]), - 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 coin_evs", - idx + 1); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); - goto EXIT; } - }; - - /* Parse diclosed keys */ - actx->disclosed_coin_secrets = GNUNET_new_array ( - actx->num_coins * (TALER_CNC_KAPPA - 1), - struct TALER_PlanchetMasterSecretP); - - json_array_foreach (j_disclosed_coin_secrets, idx, value) { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, &actx->disclosed_coin_secrets[idx]), - GNUNET_JSON_spec_end () - }; - if (GNUNET_OK != - GNUNET_JSON_parse (value, spec, NULL, NULL)) + json_array_foreach (array, k, value) { - 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_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); - goto EXIT; + 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; - *mhd_ret = MHD_YES; - EXIT: return result; @@ -269,254 +194,181 @@ EXIT: * @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_SYSERROR if we did not find the request in the DB + * @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_AgeWithdrawCommitment *commitment, + struct TALER_EXCHANGEDB_AgeWithdraw *commitment, MHD_RESULT *result) { enum GNUNET_DB_QueryStatus qs; - qs = TEH_plugin->get_age_withdraw_info (TEH_plugin->cls, - reserve_pub, - h_commitment, - commitment); - switch (qs) + for (unsigned int try = 0; try < 3; try++) { - 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); - break; - - case GNUNET_DB_STATUS_HARD_ERROR: - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_age_withdraw_info"); - break; - - case GNUNET_DB_STATUS_SOFT_ERROR: - /* FIXME:oec: Do we queue a result in this case or retry? */ - default: - GNUNET_break (0); /* should be impossible */ - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); + 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; } /** - * Check if the given denomination is still or already valid, has not been - * revoked and supports age restriction. + * @brief Derives a age-restricted planchet from a given secret and calculates the hash * - * @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] dks On success, will contain the denomination key details - * @param[out] result On failure, an MHD-response will be qeued and result will be set to accordingly - * @return true on success (denomination valid), false otherwise + * @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 bool -denomination_is_valid ( +static enum GNUNET_GenericReturnValue +calculate_blinded_hash ( struct MHD_Connection *connection, - struct TEH_KeyStateHandle *ksh, - const struct TALER_DenominationHashP *denom_h, - struct TEH_DenominationKey *dks, + 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) { - dks = TEH_keys_denomination_by_hash2 ( - ksh, - denom_h, - connection, - result); - - if (NULL == dks) - { - /* The denomination doesn't exist */ - GNUNET_assert (result != NULL); - /* Note: a HTTP-response has been queued and result has been set by - * TEH_keys_denominations_by_hash2 */ - return false; - } - - if (GNUNET_TIME_absolute_is_past (dks->meta.expire_withdraw.abs_time)) - { - /* This denomination is past the expiration time for withdraws */ - *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 (dks->meta.start.abs_time)) + 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) { - /* 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; + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; } - if (dks->recoup_possible) + /* calculate age commitment hash */ { - /* This denomination has been revoked */ - *result = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_GONE, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - NULL); - return false; + 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); } - if (0 == dks->denom_pub.age_mask.bits) + /* Next: calculate planchet */ { - /* 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_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, - msg); - return false; - } - - 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. - * - * @param connection The HTTP connection to the client - * @param len The lengths of the array @a denoms_h - * @param denoms_h array of hashes of denomination public keys - * @param[out] dks On success, will be filled with the denomination keys. Caller must deallocate. - * @param amount_with_fee The committed amount including fees - * @param[out] total_amount On success, will contain the total sum of all denominations - * @param[out] total_fee On success, will contain the total sum of all 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 *denoms_h, - struct TEH_DenominationKey **dks, - const struct TALER_Amount *amount_with_fee, - struct TALER_Amount *total_amount, - struct TALER_Amount *total_fee, - MHD_RESULT *result) -{ - struct TEH_KeyStateHandle *ksh; - - GNUNET_assert (*dks == NULL); - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - - *dks = GNUNET_new_array (len, struct TEH_DenominationKey); - TALER_amount_set_zero (TEH_currency, total_amount); - TALER_amount_set_zero (TEH_currency, total_fee); + 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; - for (uint32_t i = 0; i < len; i++) - { - if (! denomination_is_valid (connection, - ksh, - &denoms_h[i], - dks[i], - result)) + // FIXME: add logic to denom.c to do this! + if (GNUNET_CRYPTO_BSA_CS == bi.cipher) { - return GNUNET_SYSERR; - } + struct TEH_CsDeriveData cdd = { + .h_denom_pub = &denom_key->h_denom_pub, + .nonce = &nonce.cs_nonce, + }; - /* Accumulate the values */ - if (0 > TALER_amount_add ( - total_amount, - total_amount, - &dks[i]->meta.value)) - { - GNUNET_break (0); - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, - "amount"); - return GNUNET_SYSERR; + 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)); } - - /* Accumulate the withdraw fees */ - if (0 > TALER_amount_add ( - total_fee, - total_fee, - &dks[i]->meta.fees.withdraw)) + 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_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, - "fee"); - return GNUNET_SYSERR; + *result = TALER_MHD_reply_json_pack (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "{ss}", + "details", + "failed to prepare planchet from base key"); + return ret; } - } - - /* Compare the committed amount against the totals */ - { - struct TALER_Amount sum; - TALER_amount_set_zero (TEH_currency, &sum); - - GNUNET_assert (0 < TALER_amount_add ( - &sum, - total_amount, - total_fee)); - if (0 != TALER_amount_cmp (&sum, amount_with_fee)) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT, - NULL); - return GNUNET_SYSERR; - } + TALER_coin_ev_hash (&detail.blinded_planchet, + &denom_key->h_denom_pub, + bch); + TALER_blinded_planchet_free (&detail.blinded_planchet); } - return GNUNET_OK; + return ret; } /** - * Checks the validity of the disclosed coins as follows: + * @brief Checks the validity of the disclosed coins as follows: * - Derives and calculates the disclosed coins' * - public keys, * - nonces (if applicable), @@ -533,169 +385,90 @@ are_denominations_valid ( * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw * * @param connection HTTP-connection to the client - * @param h_commitment_orig Original commitment - * @param max_age Maximum age allowed for the age restriction - * @param noreveal_idx Index that was given to the client in response to the age-withdraw request - * @param num_coins Number of coins - * @param coin_evs The Hashes of the undisclosed, blinded coins, @a num_coins many - * @param denom_keys The array of denomination keys, @a num_coins. Needed to detect Clause-Schnorr-based denominations + * @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_AgeWithdrawCommitmentHashP *h_commitment_orig, - const uint32_t max_age, - const uint32_t noreveal_idx, - const uint32_t num_coins, - const struct TALER_BlindedCoinHashP *coin_evs, - const struct TEH_DenominationKey *denom_keys, + 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 c = 0; c < num_coins; c++) + for (size_t coin_idx = 0; coin_idx < num_coins; coin_idx++) { - size_t k = 0; /* either 0 or 1, to index into coin_evs */ + size_t i = 0; /* either 0 or 1, to index into coin_evs */ - for (size_t idx = 0; idx<3; idx++) + for (size_t k = 0; k<TALER_CNC_KAPPA; k++) { - if (idx == (size_t) noreveal_idx) + if (k == (size_t) commitment->noreveal_index) { GNUNET_CRYPTO_hash_context_read (hash_context, - &coin_evs[c], - sizeof(coin_evs[c])); + &commitment->h_coin_evs[coin_idx], + sizeof(commitment->h_coin_evs[coin_idx])); } else { - /* FIXME:oec: Refactor this block out into its own function */ - - size_t j = 2 * c + k; /* Index into disclosed_coin_secrets[] */ + /* 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_AgeCommitmentHash ach; + struct TALER_BlindedCoinHashP bch; - GNUNET_assert (k<2); - GNUNET_assert (num_coins * (TALER_CNC_KAPPA - 1) > j); + GNUNET_assert (2>i); + GNUNET_assert ((TALER_CNC_KAPPA - 1) * num_coins > j); secret = &disclosed_coin_secrets[j]; - k++; + i++; - /* First: calculate age commitment hash */ - { - struct TALER_AgeCommitmentProof acp; - ret = TALER_age_restriction_from_secret ( - secret, - &denom_keys[c].denom_pub.age_mask, - max_age, - &acp); - - if (GNUNET_OK != ret) - { - GNUNET_break (0); - *result = TALER_MHD_reply_json_pack (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "{sssi}", - "failed to derive age restriction from base key", - "index", - j); - return ret; - } - - TALER_age_commitment_hash (&acp.commitment, &ach); - } + ret = calculate_blinded_hash (connection, + keys, + secret, + &commitment->denom_pub_hashes[coin_idx], + commitment->max_age, + &bch, + result); - /* Next: calculate planchet */ + if (GNUNET_OK != ret) { - struct TALER_CoinPubHashP c_hash; - struct TALER_PlanchetDetail detail; - struct TALER_BlindedCoinHashP bch; - struct TALER_CoinSpendPrivateKeyP coin_priv; - union TALER_DenominationBlindingKeyP bks; - struct TALER_ExchangeWithdrawValues alg_values = { - .cipher = denom_keys[c].denom_pub.cipher, - }; - - if (TALER_DENOMINATION_CS == alg_values.cipher) - { - struct TALER_CsNonce nonce; - - TALER_cs_withdraw_nonce_derive ( - secret, - &nonce); - - { - enum TALER_ErrorCode ec; - struct TEH_CsDeriveData cdd = { - .h_denom_pub = &denom_keys[c].h_denom_pub, - .nonce = &nonce, - }; - - ec = TEH_keys_denomination_cs_r_pub (&cdd, - false, - &alg_values.details. - cs_values); - -#pragma message ("FIXME:oec: return value of needs handling!") - /* FIXME:oec: Handle error */ - GNUNET_assert (TALER_EC_NONE == ec); - } - } - - TALER_planchet_blinding_secret_create (secret, - &alg_values, - &bks); - - TALER_planchet_setup_coin_priv (secret, - &alg_values, - &coin_priv); - - ret = TALER_planchet_prepare (&denom_keys[c].denom_pub, - &alg_values, - &bks, - &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, - "{sssi}", - "details", - "failed to prepare planchet from base key", - "index", - j); - return ret; - } - - ret = TALER_coin_ev_hash (&detail.blinded_planchet, - &denom_keys[c].h_denom_pub, - &bch); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - *result = TALER_MHD_reply_json_pack (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "{sssi}", - "details", - "failed to hash planchet from base key", - "index", - j); - return ret; - } - - GNUNET_CRYPTO_hash_context_read (hash_context, - &detail.blinded_planchet, - sizeof(detail.blinded_planchet)); + 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)); } } } @@ -706,7 +479,7 @@ verify_commitment_and_max_age ( GNUNET_CRYPTO_hash_context_finish (hash_context, &calc_hash); - if (0 != GNUNET_CRYPTO_hash_cmp (&h_commitment_orig->hash, + if (0 != GNUNET_CRYPTO_hash_cmp (&commitment->h_commitment.hash, &calc_hash)) { GNUNET_break_op (0); @@ -717,8 +490,40 @@ verify_commitment_and_max_age ( } } + return GNUNET_OK; +} - return ret; + +/** + * @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)); } @@ -731,45 +536,41 @@ TEH_handler_age_withdraw_reveal ( MHD_RESULT result = MHD_NO; enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; struct AgeRevealContext actx = {0}; - json_t *j_denoms_h; - json_t *j_coin_evs; - json_t *j_disclosed_coin_secrets; + 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_json ("denoms_h", &j_denoms_h), - GNUNET_JSON_spec_json ("coin_evs", &j_coin_evs), - GNUNET_JSON_spec_json ("disclosed_coin_secrets", &j_disclosed_coin_secrets), + 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) { - 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; - } + 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 ( + if (GNUNET_OK != + parse_age_withdraw_reveal_json ( rc->connection, - j_denoms_h, - j_coin_evs, j_disclosed_coin_secrets, &actx, &result)) break; /* Find original commitment */ - if (GNUNET_OK != find_original_commitment ( + if (GNUNET_OK != + find_original_commitment ( rc->connection, &actx.ach, &actx.reserve_pub, @@ -777,38 +578,31 @@ TEH_handler_age_withdraw_reveal ( &result)) break; - /* Ensure validity of denoms and the sum of amounts and fees */ - if (GNUNET_OK != are_denominations_valid ( - rc->connection, - actx.num_coins, - actx.denoms_h, - &actx.denom_keys, - &actx.commitment.amount_with_fee, - &actx.total_amount, - &actx.total_fee, - &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 ( + /* 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.h_commitment, - actx.commitment.max_age, - actx.commitment.noreveal_index, - actx.num_coins, - actx.coin_evs, - actx.denom_keys, + &actx.commitment, actx.disclosed_coin_secrets, + actx.num_coins, &result)) break; - /* TODO:oec: sign the coins */ + /* Finally, return the signatures */ + result = reply_age_withdraw_reveal_success (rc->connection, + &actx.commitment); - } while(0); + } while (0); - age_reveal_context_free (&actx); 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; } diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h index 73ebc6fb5..f7b813fe7 100644 --- a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h +++ b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h @@ -18,8 +18,8 @@ * @brief Handle /age-withdraw/$ACH/reveal requests * @author Özgür Kesim */ -#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H -#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H +#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H +#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H #include <microhttpd.h> #include "taler-exchange-httpd.h" diff --git a/src/exchange/taler-exchange-httpd_aml-decision-get.c b/src/exchange/taler-exchange-httpd_aml-decision-get.c index 6b36fe27f..b4f337db1 100644 --- a/src/exchange/taler-exchange-httpd_aml-decision-get.c +++ b/src/exchange/taler-exchange-httpd_aml-decision-get.c @@ -43,8 +43,6 @@ * @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 birthdate birthdate of user, in format YYYY-MM-DD; can be NULL; - * digits can be 0 if exact day, month or year are unknown * @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 @@ -55,7 +53,6 @@ kyc_attribute_cb ( void *cls, const struct TALER_PaytoHashP *h_payto, const char *provider_section, - const char *birthdate, struct GNUNET_TIME_Timestamp collection_time, struct GNUNET_TIME_Timestamp expiration_time, size_t enc_attributes_size, diff --git a/src/exchange/taler-exchange-httpd_aml-decision.c b/src/exchange/taler-exchange-httpd_aml-decision.c index 2830e54ed..bf43fdbf2 100644 --- a/src/exchange/taler-exchange-httpd_aml-decision.c +++ b/src/exchange/taler-exchange-httpd_aml-decision.c @@ -74,7 +74,7 @@ struct DecisionContext /** * KYC requirements imposed, NULL for none. */ - json_t *kyc_requirements; + const json_t *kyc_requirements; }; @@ -165,6 +165,7 @@ make_aml_decision (void *cls, TEH_plugin->cls, res, &dc->h_payto, + NULL, /* not a reserve */ &requirement_row); if (qs < 0) { @@ -245,7 +246,6 @@ TEH_handler_post_aml_decision ( struct DecisionContext dc = { .officer_pub = officer_pub }; - uint32_t new_state32; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("officer_sig", &dc.officer_sig), @@ -258,11 +258,11 @@ TEH_handler_post_aml_decision ( &dc.justification), GNUNET_JSON_spec_timestamp ("decision_time", &dc.decision_time), - GNUNET_JSON_spec_uint32 ("new_state", - &new_state32), + TALER_JSON_spec_aml_decision ("new_state", + &dc.new_state), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("kyc_requirements", - &dc.kyc_requirements), + GNUNET_JSON_spec_array_const ("kyc_requirements", + &dc.kyc_requirements), NULL), GNUNET_JSON_spec_end () }; @@ -281,7 +281,6 @@ TEH_handler_post_aml_decision ( return MHD_YES; /* failure */ } } - dc.new_state = (enum TALER_AmlDecisionState) new_state32; TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_officer_aml_decision_verify (dc.justification, @@ -306,17 +305,6 @@ TEH_handler_post_aml_decision ( size_t index; json_t *elem; - if (! json_is_array (dc.kyc_requirements)) - { - 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, - "kyc_requirements must be an array"); - } - json_array_foreach (dc.kyc_requirements, index, elem) { const char *val; @@ -324,7 +312,6 @@ TEH_handler_post_aml_decision ( if (! json_is_string (elem)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, @@ -336,7 +323,6 @@ TEH_handler_post_aml_decision ( TALER_KYCLOGIC_check_satisfiable (val)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, @@ -357,11 +343,9 @@ TEH_handler_post_aml_decision ( &make_aml_decision, &dc)) { - GNUNET_JSON_parse_free (spec); return mhd_ret; } } - GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_static ( connection, MHD_HTTP_NO_CONTENT, diff --git a/src/exchange/taler-exchange-httpd_aml-decisions-get.c b/src/exchange/taler-exchange-httpd_aml-decisions-get.c index 0183ac3b8..763817cf6 100644 --- a/src/exchange/taler-exchange-httpd_aml-decisions-get.c +++ b/src/exchange/taler-exchange-httpd_aml-decisions-get.c @@ -81,7 +81,7 @@ TEH_handler_aml_decisions_get ( { enum TALER_AmlDecisionState decision; int delta = -20; - unsigned long long start = INT64_MAX; + unsigned long long start; const char *state_str = args[0]; if (NULL == state_str) @@ -123,40 +123,44 @@ TEH_handler_aml_decisions_get ( p = MHD_lookup_connection_value (rc->connection, MHD_GET_ARGUMENT_KIND, - "start"); + "delta"); if (NULL != p) { char dummy; if (1 != sscanf (p, - "%llu%c", - &start, + "%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, - "start"); + "delta"); } } + if (delta > 0) + start = 0; + else + start = INT64_MAX; p = MHD_lookup_connection_value (rc->connection, MHD_GET_ARGUMENT_KIND, - "delta"); + "start"); if (NULL != p) { char dummy; if (1 != sscanf (p, - "%d%c", - &delta, + "%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, - "delta"); + "start"); } } } diff --git a/src/exchange/taler-exchange-httpd_batch-deposit.c b/src/exchange/taler-exchange-httpd_batch-deposit.c index d000c454f..84f27dd94 100644 --- a/src/exchange/taler-exchange-httpd_batch-deposit.c +++ b/src/exchange/taler-exchange-httpd_batch-deposit.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2022 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 @@ -28,9 +28,10 @@ #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_deposit.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" @@ -41,10 +42,11 @@ */ struct BatchDepositContext { + /** - * Information about the individual coin deposits. + * Array with the individual coin deposit fees. */ - struct TALER_EXCHANGEDB_Deposit *deposits; + struct TALER_Amount *deposit_fees; /** * Our timestamp (when we received the request). @@ -54,37 +56,22 @@ struct BatchDepositContext struct GNUNET_TIME_Timestamp exchange_timestamp; /** - * Hash over the proposal data between merchant and customer - * (remains unknown to the Exchange). - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Public key of the merchant. Enables later identification - * of the merchant in case of a need to rollback transactions. - */ - struct TALER_MerchantPublicKeyP merchant_pub; - - /** - * Salt used by the merchant to compute @e h_wire. + * Details about the batch deposit operation. */ - struct TALER_WireSaltP wire_salt; + struct TALER_EXCHANGEDB_BatchDeposit bd; - /** - * Hash over the wire details (with @e wire_salt). - */ - struct TALER_MerchantWireHashP h_wire; /** - * Hash of the payto URI. + * Total amount that is accumulated with this deposit, + * without fee. */ - struct TALER_PaytoHashP h_payto; + struct TALER_Amount accumulated_total_without_fee; /** - * Information about the receiver for executing the transaction. URI in - * payto://-format. + * True, if no policy was present in the request. Then + * @e policy_json is NULL and @e h_policy will be all zero. */ - const char *payto_uri; + bool has_no_policy; /** * Additional details for policy extension relevant for this @@ -93,9 +80,11 @@ struct BatchDepositContext json_t *policy_json; /** - * Will be true if policy_json were provided + * If @e policy_json was present, the corresponding policy extension + * calculates these details. These will be persisted in the policy_details + * table. */ - bool has_policy; + struct TALER_PolicyDetails policy_details; /** * Hash over @e policy_details, might be all zero @@ -103,11 +92,9 @@ struct BatchDepositContext struct TALER_ExtensionPolicyHashP h_policy; /** - * If @e policy_json was present, the corresponding policy extension - * calculates these details. These will be persisted in the policy_details - * table. + * Hash over the merchant's payto://-URI with the wire salt. */ - struct TALER_PolicyDetails policy_details; + struct TALER_MerchantWireHashP h_wire; /** * When @e policy_details are persisted, this contains the id of the record @@ -115,40 +102,6 @@ struct BatchDepositContext */ uint64_t policy_details_serial_id; - /** - * Time when this request was generated. Used, for example, to - * assess when (roughly) the income was achieved for tax purposes. - * Note that the Exchange will only check that the timestamp is not "too - * far" into the future (i.e. several days). The fact that the - * timestamp falls within the validity period of the coin's - * denomination key is irrelevant for the validity of the deposit - * request, as obviously the customer and merchant could conspire to - * set any timestamp. Also, the Exchange must accept very old deposit - * requests, as the merchant might have been unable to transmit the - * deposit request in a timely fashion (so back-dating is not - * prevented). - */ - struct GNUNET_TIME_Timestamp timestamp; - - /** - * How much time does the merchant have to issue a refund request? - * Zero if refunds are not allowed. After this time, the coin - * cannot be refunded. - */ - struct GNUNET_TIME_Timestamp refund_deadline; - - /** - * How much time does the merchant have to execute the wire transfer? - * This time is advisory for aggregating transactions, not a hard - * constraint (as the merchant can theoretically pick any time, - * including one in the past). - */ - struct GNUNET_TIME_Timestamp wire_deadline; - - /** - * Number of coins in the batch. - */ - unsigned int num_coins; }; @@ -160,83 +113,52 @@ struct BatchDepositContext * requested batch deposit operation with the given wiring details. * * @param connection connection to the client - * @param bdc information about the batch deposit + * @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 *bdc) + const struct BatchDepositContext *dc) { - json_t *arr; + 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; - -again: - arr = json_array (); - GNUNET_assert (NULL != arr); - for (unsigned int i = 0; i<bdc->num_coins; i++) + 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))) { - const struct TALER_EXCHANGEDB_Deposit *deposit = &bdc->deposits[i]; - struct TALER_ExchangePublicKeyP pubi; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec; - struct TALER_Amount amount_without_fee; - - GNUNET_assert (0 <= - TALER_amount_subtract (&amount_without_fee, - &deposit->amount_with_fee, - &deposit->deposit_fee)); - if (TALER_EC_NONE != - (ec = TALER_exchange_online_deposit_confirmation_sign ( - &TEH_keys_exchange_sign_, - &bdc->h_contract_terms, - &bdc->h_wire, - bdc->has_policy ? &bdc->h_policy: NULL, - bdc->exchange_timestamp, - bdc->wire_deadline, - bdc->refund_deadline, - &amount_without_fee, - &deposit->coin.coin_pub, - &bdc->merchant_pub, - &pubi, - &sig))) - { - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - if (0 == i) - pub = pubi; - if (0 != - GNUNET_memcmp (&pub, - &pubi)) - { - /* note: in the future, maybe have batch - sign API to avoid having to handle - key rollover... */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange public key changed during batch deposit, trying again\n"); - json_decref (arr); - goto again; - } - GNUNET_assert ( - 0 == - json_array_append_new (arr, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ( - "exchange_sig", - &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", - bdc->exchange_timestamp), + dc->exchange_timestamp), GNUNET_JSON_pack_data_auto ("exchange_pub", &pub), - GNUNET_JSON_pack_array_steal ("exchange_sigs", - arr)); + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig)); } @@ -259,79 +181,113 @@ batch_deposit_transaction (void *cls, MHD_RESULT *mhd_ret) { struct BatchDepositContext *dc = cls; - enum GNUNET_DB_QueryStatus qs = GNUNET_SYSERR; + 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_policy) + if (! dc->has_no_policy) { qs = TEH_plugin->persist_policy_details ( TEH_plugin->cls, &dc->policy_details, - &dc->policy_details_serial_id, - &dc->policy_details.accumulated_total, + &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; } - for (unsigned int i = 0; i<dc->num_coins; i++) + /* FIXME: replace by batch insert! */ + for (unsigned int i = 0; i<bd->num_cdis; i++) { - const struct TALER_EXCHANGEDB_Deposit *deposit = &dc->deposits[i]; + const struct TALER_EXCHANGEDB_CoinDepositInformation *cdi + = &bd->cdis[i]; uint64_t known_coin_id; - qs = TEH_make_coin_known (&deposit->coin, + 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, - deposit, - known_coin_id, - &dc->h_payto, - dc->has_policy - ? &dc->policy_details_serial_id - : NULL, - &dc->exchange_timestamp, - &balance_ok, - &in_conflict); - if (qs < 0) + } + + 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)) { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; TALER_LOG_WARNING ( - "Failed to store /batch-deposit information in database\n"); + "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; } - if (in_conflict) - { - /* FIXME: #7267 conficting contract != insufficient funds */ - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT, - &deposit->coin.denom_pub_hash, - &deposit->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &deposit->coin.denom_pub_hash, - &deposit->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } + + *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; @@ -344,34 +300,37 @@ batch_deposit_transaction (void *cls, * @a ctx. * * @param connection connection we are handling + * @param dc information about the overall batch * @param jcoin coin data to parse - * @param dc overall batch deposit context information to use - * @param[out] deposit where to store the result + * @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, - json_t *jcoin, const struct BatchDepositContext *dc, - struct TALER_EXCHANGEDB_Deposit *deposit) + 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, - &deposit->amount_with_fee), + &cdi->amount_with_fee), GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &deposit->coin.denom_pub_hash), + &cdi->coin.denom_pub_hash), TALER_JSON_spec_denom_sig ("ub_sig", - &deposit->coin.denom_sig), + &cdi->coin.denom_sig), GNUNET_JSON_spec_fixed_auto ("coin_pub", - &deposit->coin.coin_pub), + &cdi->coin.coin_pub), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &deposit->coin.h_age_commitment), - &deposit->coin.no_age_commitment), + &cdi->coin.h_age_commitment), + &cdi->coin.no_age_commitment), GNUNET_JSON_spec_fixed_auto ("coin_sig", - &deposit->csig), + &cdi->csig), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; @@ -386,16 +345,18 @@ parse_coin (struct MHD_Connection *connection, struct TEH_DenominationKey *dk; MHD_RESULT mret; - dk = TEH_keys_denomination_by_hash (&deposit->coin.denom_pub_hash, + dk = TEH_keys_denomination_by_hash (&cdi->coin.denom_pub_hash, connection, &mret); if (NULL == dk) { GNUNET_JSON_parse_free (spec); - return mret; + return (MHD_YES == mret) + ? GNUNET_NO + : GNUNET_SYSERR; } if (0 > TALER_amount_cmp (&dk->meta.value, - &deposit->amount_with_fee)) + &cdi->amount_with_fee)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -414,7 +375,7 @@ parse_coin (struct MHD_Connection *connection, return (MHD_YES == TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, - &deposit->coin.denom_pub_hash, + &cdi->coin.denom_pub_hash, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "DEPOSIT")) ? GNUNET_NO @@ -427,7 +388,7 @@ parse_coin (struct MHD_Connection *connection, return (MHD_YES == TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, - &deposit->coin.denom_pub_hash, + &cdi->coin.denom_pub_hash, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, "DEPOSIT")) ? GNUNET_NO @@ -440,13 +401,14 @@ parse_coin (struct MHD_Connection *connection, return (MHD_YES == TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, - &deposit->coin.denom_pub_hash, + &cdi->coin.denom_pub_hash, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, "DEPOSIT")) ? GNUNET_NO : GNUNET_SYSERR; } - if (dk->denom_pub.cipher != deposit->coin.denom_sig.cipher) + 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); @@ -459,21 +421,21 @@ parse_coin (struct MHD_Connection *connection, : GNUNET_SYSERR; } - deposit->deposit_fee = dk->meta.fees.deposit; + *deposit_fee = dk->meta.fees.deposit; /* check coin signature */ - switch (dk->denom_pub.cipher) + switch (dk->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; break; default: break; } if (GNUNET_YES != - TALER_test_coin_valid (&deposit->coin, + TALER_test_coin_valid (&cdi->coin, &dk->denom_pub)) { TALER_LOG_WARNING ("Invalid coin passed for /batch-deposit\n"); @@ -487,8 +449,8 @@ parse_coin (struct MHD_Connection *connection, : GNUNET_SYSERR; } } - if (0 < TALER_amount_cmp (&deposit->deposit_fee, - &deposit->amount_with_fee)) + if (0 < TALER_amount_cmp (deposit_fee, + &cdi->amount_with_fee)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -504,18 +466,21 @@ parse_coin (struct MHD_Connection *connection, TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_deposit_verify ( - &deposit->amount_with_fee, - &deposit->deposit_fee, + &cdi->amount_with_fee, + deposit_fee, &dc->h_wire, - &dc->h_contract_terms, - &deposit->coin.h_age_commitment, - dc->has_policy ? &dc->h_policy : NULL, - &deposit->coin.denom_pub_hash, - dc->timestamp, - &dc->merchant_pub, - dc->refund_deadline, - &deposit->coin.coin_pub, - &deposit->csig)) + &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); @@ -523,17 +488,10 @@ parse_coin (struct MHD_Connection *connection, TALER_MHD_reply_with_error (connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID, - TALER_B2S (&deposit->coin.coin_pub))) + TALER_B2S (&cdi->coin.coin_pub))) ? GNUNET_NO : GNUNET_SYSERR; } - deposit->merchant_pub = dc->merchant_pub; - deposit->h_contract_terms = dc->h_contract_terms; - deposit->wire_salt = dc->wire_salt; - deposit->receiver_wire_account = (char *) dc->payto_uri; - deposit->timestamp = dc->timestamp; - deposit->refund_deadline = dc->refund_deadline; - deposit->wire_deadline = dc->wire_deadline; return GNUNET_OK; } @@ -544,62 +502,64 @@ TEH_handler_batch_deposit (struct TEH_RequestContext *rc, const char *const args[]) { struct MHD_Connection *connection = rc->connection; - struct BatchDepositContext dc; - json_t *coins; + struct BatchDepositContext dc = { 0 }; + struct TALER_EXCHANGEDB_BatchDeposit *bd = &dc.bd; + const json_t *coins; bool no_refund_deadline = true; - bool no_policy_json = true; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("merchant_payto_uri", - &dc.payto_uri), + TALER_JSON_spec_payto_uri ("merchant_payto_uri", + &bd->receiver_wire_account), GNUNET_JSON_spec_fixed_auto ("wire_salt", - &dc.wire_salt), + &bd->wire_salt), GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &dc.merchant_pub), + &bd->merchant_pub), GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &dc.h_contract_terms), - GNUNET_JSON_spec_json ("coins", - &coins), + &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), - &no_policy_json), + &dc.has_no_policy), GNUNET_JSON_spec_timestamp ("timestamp", - &dc.timestamp), + &bd->wallet_timestamp), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("refund_deadline", - &dc.refund_deadline), + &bd->refund_deadline), &no_refund_deadline), GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &dc.wire_deadline), + &bd->wire_deadline), GNUNET_JSON_spec_end () }; - enum GNUNET_GenericReturnValue res; (void) args; - memset (&dc, - 0, - sizeof (dc)); - 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 */ - } + enum GNUNET_GenericReturnValue res; - dc.has_policy = ! no_policy_json; + 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 (dc.payto_uri); + emsg = TALER_payto_validate (bd->receiver_wire_account); if (NULL != emsg) { MHD_RESULT ret; @@ -614,9 +574,9 @@ TEH_handler_batch_deposit (struct TEH_RequestContext *rc, return ret; } } - if (GNUNET_TIME_timestamp_cmp (dc.refund_deadline, + if (GNUNET_TIME_timestamp_cmp (bd->refund_deadline, >, - dc.wire_deadline)) + bd->wire_deadline)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -625,7 +585,7 @@ TEH_handler_batch_deposit (struct TEH_RequestContext *rc, TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE, NULL); } - if (GNUNET_TIME_absolute_is_never (dc.wire_deadline.abs_time)) + if (GNUNET_TIME_absolute_is_never (bd->wire_deadline.abs_time)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -634,33 +594,42 @@ TEH_handler_batch_deposit (struct TEH_RequestContext *rc, TALER_EC_EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER, NULL); } - TALER_payto_hash (dc.payto_uri, - &dc.h_payto); - TALER_merchant_wire_signature_hash (dc.payto_uri, - &dc.wire_salt, + 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_policy) + 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); } - dc.num_coins = json_array_size (coins); - if (0 == dc.num_coins) + bd->num_cdis = json_array_size (coins); + if (0 == bd->num_cdis) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -669,7 +638,7 @@ TEH_handler_batch_deposit (struct TEH_RequestContext *rc, TALER_EC_GENERIC_PARAMETER_MALFORMED, "coins"); } - if (TALER_MAX_FRESH_COINS < dc.num_coins) + if (TALER_MAX_FRESH_COINS < bd->num_cdis) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -678,90 +647,91 @@ TEH_handler_batch_deposit (struct TEH_RequestContext *rc, TALER_EC_GENERIC_PARAMETER_MALFORMED, "coins"); } - dc.deposits = GNUNET_new_array (dc.num_coins, - struct TALER_EXCHANGEDB_Deposit); - for (unsigned int i = 0; i<dc.num_coins; i++) + { - do { + 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, - json_array_get (coins, i), &dc, - &dc.deposits[i]); + json_array_get (coins, + i), + &cdis[i], + &deposit_fees[i]); if (GNUNET_OK != res) break; - - /* If applicable, accumulate all contributions into the policy_details */ - if (dc.has_policy) - { - /* FIXME: how do deposit-fee and policy-fee interact? */ - struct TALER_Amount amount_without_fee; - - res = TALER_amount_subtract (&amount_without_fee, - &dc.deposits[i].amount_with_fee, - &dc.deposits[i].deposit_fee - ); - res = TALER_amount_add ( - &dc.policy_details.accumulated_total, - &dc.policy_details.accumulated_total, - &amount_without_fee); - } - } while(0); - + 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 (&dc.deposits[j].coin.denom_sig); - GNUNET_free (dc.deposits); + 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"); - } + 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; + /* 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; + } + } - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "execute batch deposit", - TEH_MT_REQUEST_BATCH_DEPOSIT, - &mhd_ret, - &batch_deposit_transaction, - &dc)) + /* generate regular response */ { - GNUNET_JSON_parse_free (spec); - for (unsigned int j = 0; j<dc.num_coins; j++) - TALER_denom_sig_free (&dc.deposits[j].coin.denom_sig); - GNUNET_free (dc.deposits); + 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; } } - - /* generate regular response */ - { - MHD_RESULT res; - - res = reply_batch_deposit_success (connection, - &dc); - for (unsigned int j = 0; j<dc.num_coins; j++) - TALER_denom_sig_free (&dc.deposits[j].coin.denom_sig); - GNUNET_free (dc.deposits); - GNUNET_JSON_parse_free (spec); - return res; - } } diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c index 6cd467d51..2b80c2fc4 100644 --- a/src/exchange/taler-exchange-httpd_batch-withdraw.c +++ b/src/exchange/taler-exchange-httpd_batch-withdraw.c @@ -26,12 +26,14 @@ #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" /** @@ -73,11 +75,16 @@ struct BatchWithdrawContext { /** - * Public key of the reserv. + * 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; @@ -184,6 +191,99 @@ aml_amount_cb ( /** + * 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, @@ -208,8 +308,11 @@ batch_withdraw_transaction (void *cls, struct BatchWithdrawContext *wc = cls; uint64_t ruuid; enum GNUNET_DB_QueryStatus qs; - bool balance_ok = false; 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; @@ -320,60 +423,63 @@ batch_withdraw_transaction (void *cls, "reserves_get_origin"); return qs; } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - *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; - } - 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) + /* 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) { - 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, + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, &wc->h_payto, - &wc->kyc.requirement_row); - GNUNET_free (kyc_required); + 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_STORE_FAILED, - "insert_kyc_requirement_for_account"); + 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; } - 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) { @@ -395,12 +501,29 @@ batch_withdraw_transaction (void *cls, 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; @@ -411,14 +534,22 @@ batch_withdraw_transaction (void *cls, { struct PlanchetContext *pc = &wc->planchets[i]; const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet; - const struct TALER_CsNonce *nonce; + const union GNUNET_CRYPTO_BlindSessionNonce *nonce = NULL; bool denom_unknown = true; bool conflict = true; bool nonce_reuse = true; - nonce = (TALER_DENOMINATION_CS == bp->cipher) - ? &bp->details.cs_blinded_planchet.nonce - : NULL; + 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, @@ -433,7 +564,7 @@ batch_withdraw_transaction (void *cls, *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_withdraw"); + "do_batch_withdraw_insert"); return qs; } if (denom_unknown) @@ -448,12 +579,18 @@ batch_withdraw_transaction (void *cls, if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || (conflict) ) { - 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); + 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) @@ -472,99 +609,6 @@ batch_withdraw_transaction (void *cls, /** - * 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 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 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 TEH_RequestContext *rc, - const struct BatchWithdrawContext *wc, - MHD_RESULT *mret) -{ - 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; -} - - -/** * The request was parsed successfully. Prepare * our side for the main DB transaction. * @@ -590,8 +634,8 @@ prepare_transaction (const struct TEH_RequestContext *rc, enum TALER_ErrorCode ec; ec = TEH_keys_denomination_batch_sign ( - csds, wc->planchets_length, + csds, false, bss); if (TALER_EC_NONE != ec) @@ -691,8 +735,7 @@ parse_planchets (const struct TEH_RequestContext *rc, ksh = TEH_keys_get_state (); if (NULL == ksh) { - if (! check_request_idempotent (rc, - wc, + if (! check_request_idempotent (wc, &mret)) { return TALER_MHD_reply_with_error (rc->connection, @@ -707,14 +750,15 @@ parse_planchets (const struct TEH_RequestContext *rc, struct PlanchetContext *pc = &wc->planchets[i]; struct TEH_DenominationKey *dk; - dk = TEH_keys_denomination_by_hash2 (ksh, - &pc->collectable.denom_pub_hash, - NULL, - NULL); + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + &pc->collectable.denom_pub_hash, + NULL, + NULL); + if (NULL == dk) { - if (! check_request_idempotent (rc, - wc, + if (! check_request_idempotent (wc, &mret)) { return TEH_RESPONSE_reply_unknown_denom_pub_hash ( @@ -726,8 +770,7 @@ parse_planchets (const struct TEH_RequestContext *rc, 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, + if (! check_request_idempotent (wc, &mret)) { return TEH_RESPONSE_reply_expired_denom_pub_hash ( @@ -751,8 +794,7 @@ parse_planchets (const struct TEH_RequestContext *rc, if (dk->recoup_possible) { /* This denomination has been revoked */ - if (! check_request_idempotent (rc, - wc, + if (! check_request_idempotent (wc, &mret)) { return TEH_RESPONSE_reply_expired_denom_pub_hash ( @@ -763,7 +805,8 @@ parse_planchets (const struct TEH_RequestContext *rc, } return mret; } - if (dk->denom_pub.cipher != pc->blinded_planchet.cipher) + 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); @@ -795,17 +838,10 @@ parse_planchets (const struct TEH_RequestContext *rc, NULL); } - if (GNUNET_OK != - TALER_coin_ev_hash (&pc->blinded_planchet, - &pc->collectable.denom_pub_hash, - &pc->collectable.h_coin_envelope)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - 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, @@ -832,21 +868,20 @@ TEH_handler_batch_withdraw (struct TEH_RequestContext *rc, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *root) { - struct BatchWithdrawContext wc; - json_t *planchets; + struct BatchWithdrawContext wc = { + .reserve_pub = reserve_pub, + .rc = rc + }; + const json_t *planchets; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("planchets", - &planchets), + GNUNET_JSON_spec_array_const ("planchets", + &planchets), GNUNET_JSON_spec_end () }; - memset (&wc, - 0, - sizeof (wc)); GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &wc.batch_total)); - wc.reserve_pub = reserve_pub; { enum GNUNET_GenericReturnValue res; @@ -856,20 +891,17 @@ TEH_handler_batch_withdraw (struct TEH_RequestContext *rc, if (GNUNET_OK != res) return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; } - if ( (! json_is_array (planchets)) || - (0 == json_array_size (planchets)) ) + wc.planchets_length = json_array_size (planchets); + if (0 == wc.planchets_length) { - GNUNET_JSON_parse_free (spec); GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "planchets"); } - wc.planchets_length = json_array_size (planchets); if (wc.planchets_length > TALER_MAX_FRESH_COINS) { - GNUNET_JSON_parse_free (spec); GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, @@ -895,7 +927,6 @@ TEH_handler_batch_withdraw (struct TEH_RequestContext *rc, TALER_blinded_planchet_free (&pc->blinded_planchet); TALER_blinded_denom_sig_free (&pc->collectable.sig); } - GNUNET_JSON_parse_free (spec); return ret; } } 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_reserves_status.h b/src/exchange/taler-exchange-httpd_coins_get.h index 831b270f7..90405b55d 100644 --- a/src/exchange/taler-exchange-httpd_reserves_status.h +++ b/src/exchange/taler-exchange-httpd_coins_get.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2020 Taler Systems SA + 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 @@ -14,30 +14,40 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-exchange-httpd_reserves_status.h - * @brief Handle /reserves/$RESERVE_PUB STATUS requests + * @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_RESERVES_STATUS_H -#define TALER_EXCHANGE_HTTPD_RESERVES_STATUS_H +#ifndef TALER_EXCHANGE_HTTPD_COINS_GET_H +#define TALER_EXCHANGE_HTTPD_COINS_GET_H #include <microhttpd.h> #include "taler-exchange-httpd.h" /** - * Handle a POST "/reserves/$RID/status" request. + * 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 reserve_pub public key of the reserve - * @param root uploaded body from the client + * @param coin_pub public key of the coin * @return MHD result code */ MHD_RESULT -TEH_handler_reserves_status (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); +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 index 0c91f0bc9..898e23dd9 100644 --- a/src/exchange/taler-exchange-httpd_common_deposit.c +++ b/src/exchange/taler-exchange-httpd_common_deposit.c @@ -135,7 +135,8 @@ TEH_common_purse_deposit_parse_coin ( "PURSE CREATE")) ? GNUNET_NO : GNUNET_SYSERR; } - if (dk->denom_pub.cipher != coin->cpi.denom_sig.cipher) + 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); @@ -164,12 +165,12 @@ TEH_common_purse_deposit_parse_coin ( &coin->deposit_fee)); /* check coin signature */ - switch (dk->denom_pub.cipher) + switch (dk->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; break; default: 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 index da5bf9691..257dfa6ba 100644 --- a/src/exchange/taler-exchange-httpd_config.c +++ b/src/exchange/taler-exchange-httpd_config.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015-2021 Taler Systems SA + Copyright (C) 2015-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -33,18 +33,55 @@ 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, diff --git a/src/exchange/taler-exchange-httpd_config.h b/src/exchange/taler-exchange-httpd_config.h index 7763cdeb5..068f51d41 100644 --- a/src/exchange/taler-exchange-httpd_config.h +++ b/src/exchange/taler-exchange-httpd_config.h @@ -41,7 +41,7 @@ * * Returned via both /config and /keys endpoints. */ -#define EXCHANGE_PROTOCOL_VERSION "14:0:2" +#define EXCHANGE_PROTOCOL_VERSION "19:2:2" /** diff --git a/src/exchange/taler-exchange-httpd_csr.c b/src/exchange/taler-exchange-httpd_csr.c index 29f83c2c9..e4fa4f5e4 100644 --- a/src/exchange/taler-exchange-httpd_csr.c +++ b/src/exchange/taler-exchange-httpd_csr.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2022 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 @@ -21,6 +21,7 @@ * @brief Handle /csr requests * @author Lucien Heuzeveldt * @author Gian Demarmles + * @author Christian Grothoff */ #include "platform.h" #include <gnunet/gnunet_util_lib.h> @@ -39,12 +40,12 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, { struct TALER_RefreshMasterSecretP rms; unsigned int csr_requests_num; - json_t *csr_requests; + const json_t *csr_requests; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("rms", &rms), - GNUNET_JSON_spec_json ("nks", - &csr_requests), + GNUNET_JSON_spec_array_const ("nks", + &csr_requests), GNUNET_JSON_spec_end () }; enum TALER_ErrorCode ec; @@ -65,7 +66,7 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, if ( (TALER_MAX_FRESH_COINS <= csr_requests_num) || (0 == csr_requests_num) ) { - GNUNET_JSON_parse_free (spec); + GNUNET_break_op (0); return TALER_MHD_reply_with_error ( rc->connection, MHD_HTTP_BAD_REQUEST, @@ -74,12 +75,12 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, } { - struct TALER_ExchangeWithdrawValues ewvs[csr_requests_num]; + struct GNUNET_CRYPTO_BlindingInputValues ewvs[csr_requests_num]; { - struct TALER_CsNonce nonces[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 TALER_DenominationCSPublicRPairP r_pubs[csr_requests_num]; + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[csr_requests_num]; for (unsigned int i = 0; i < csr_requests_num; i++) { @@ -101,22 +102,20 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, -1); if (GNUNET_OK != res) { - GNUNET_JSON_parse_free (spec); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } TALER_cs_refresh_nonce_derive (&rms, coin_off, &nonces[i]); } - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i < csr_requests_num; i++) { - const struct TALER_CsNonce *nonce = &nonces[i]; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = &nonces[i]; const struct TALER_DenominationHashP *denom_pub_hash = &denom_pub_hashes[i]; - ewvs[i].cipher = TALER_DENOMINATION_CS; + ewvs[i].cipher = GNUNET_CRYPTO_BSA_CS; /* check denomination referenced by denom_pub_hash */ { struct TEH_KeyStateHandle *ksh; @@ -129,10 +128,10 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); } - dk = TEH_keys_denomination_by_hash2 (ksh, - denom_pub_hash, - NULL, - 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 ( @@ -167,7 +166,8 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, "csr-melt"); } - if (TALER_DENOMINATION_CS != dk->denom_pub.cipher) + 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 ( @@ -178,8 +178,8 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, cdds[i].h_denom_pub = denom_pub_hash; cdds[i].nonce = nonce; } /* for (i) */ - ec = TEH_keys_denomination_cs_batch_r_pub (cdds, - csr_requests_num, + ec = TEH_keys_denomination_cs_batch_r_pub (csr_requests_num, + cdds, true, r_pubs); if (TALER_EC_NONE != ec) @@ -202,10 +202,13 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, 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", - &ewvs[i])); + &exw)); GNUNET_assert (NULL != csr_obj); GNUNET_assert (0 == json_array_append_new (csr_response_ewvs, @@ -228,10 +231,10 @@ TEH_handler_csr_withdraw (struct TEH_RequestContext *rc, const json_t *root, const char *const args[]) { - struct TALER_CsNonce nonce; + struct GNUNET_CRYPTO_CsSessionNonce nonce; struct TALER_DenominationHashP denom_pub_hash; - struct TALER_ExchangeWithdrawValues ewv = { - .cipher = TALER_DENOMINATION_CS + struct GNUNET_CRYPTO_BlindingInputValues ewv = { + .cipher = GNUNET_CRYPTO_BSA_CS }; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("nonce", @@ -264,10 +267,10 @@ TEH_handler_csr_withdraw (struct TEH_RequestContext *rc, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); } - dk = TEH_keys_denomination_by_hash2 (ksh, - &denom_pub_hash, - NULL, - 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 ( @@ -302,7 +305,8 @@ TEH_handler_csr_withdraw (struct TEH_RequestContext *rc, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, "csr-withdraw"); } - if (TALER_DENOMINATION_CS != dk->denom_pub.cipher) + 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 ( @@ -330,12 +334,17 @@ TEH_handler_csr_withdraw (struct TEH_RequestContext *rc, 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", - &ewv)); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + TALER_JSON_pack_exchange_withdraw_values ("ewv", + &exw)); + } } diff --git a/src/exchange/taler-exchange-httpd_db.c b/src/exchange/taler-exchange-httpd_db.c index 5660074ee..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" @@ -37,14 +40,14 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, { enum TALER_EXCHANGEDB_CoinKnownStatus cks; struct TALER_DenominationHashP h_denom_pub; - struct TALER_AgeCommitmentHash age_hash; + 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: @@ -61,22 +64,50 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, NULL); return GNUNET_DB_STATUS_HARD_ERROR; case TALER_EXCHANGEDB_CKS_DENOM_CONFLICT: - /* FIXME: insufficient_funds != denom conflict! See issue #7267, need new - * strategy for evidence gathering */ - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, - &h_denom_pub, - &coin->coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - case TALER_EXCHANGEDB_CKS_AGE_CONFLICT: - /* FIXME: insufficient_funds != Age conflict! See issue #7267, need new - * strategy for evidence gathering */ - *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, + cks, &h_denom_pub, - &coin->coin_pub); + &coin->coin_pub, + &h_age_commitment); return GNUNET_DB_STATUS_HARD_ERROR; } GNUNET_assert (0); diff --git a/src/exchange/taler-exchange-httpd_deposit.c b/src/exchange/taler-exchange-httpd_deposit.c deleted file mode 100644 index 740db7c1f..000000000 --- a/src/exchange/taler-exchange-httpd_deposit.c +++ /dev/null @@ -1,569 +0,0 @@ -/* - 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_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 - * @author Özgür Kesim - */ -#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_policy hash of applicable policy extension - * @param h_contract_terms hash of contract details - * @param exchange_timestamp exchange's timestamp - * @param refund_deadline until when this deposit be refunded - * @param wire_deadline until when will the exchange wire the funds - * @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_MerchantWireHashP *h_wire, - const struct TALER_ExtensionPolicyHashP *h_policy, - const struct TALER_PrivateContractHashP *h_contract_terms, - struct GNUNET_TIME_Timestamp exchange_timestamp, - struct GNUNET_TIME_Timestamp 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; - enum TALER_ErrorCode ec; - - ec = TALER_exchange_online_deposit_confirmation_sign ( - &TEH_keys_exchange_sign_, - h_contract_terms, - h_wire, - h_policy, - exchange_timestamp, - wire_deadline, - refund_deadline, - amount_without_fee, - coin_pub, - merchant, - &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_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_PaytoHashP h_payto; - - /** - * Row of of the coin in the known_coins table. - */ - uint64_t known_coin_id; - - /* - * True if @e policy_json was provided - */ - bool has_policy; - - /** - * If @e has_policy is true, the corresponding policy extension calculates - * these details. These will be persisted in the policy_details table. - */ - struct TALER_PolicyDetails policy_details; - - /** - * Hash over the policy data for this deposit (remains unknown to the - * Exchange). Needed for the verification of the deposit's signature - */ - struct TALER_ExtensionPolicyHashP h_policy; - - /** - * When has_policy is true, and deposit->policy_details are - * persisted, this contains the id of the record in the policy_details table. - */ - uint64_t policy_details_serial_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_make_coin_known (&dc->deposit->coin, - connection, - &dc->known_coin_id, - mhd_ret); - if (qs < 0) - return qs; - /* If the deposit has a policy associated to it, persist it. This will - * insert or update the record. */ - if (dc->has_policy) - { - qs = TEH_plugin->persist_policy_details ( - TEH_plugin->cls, - &dc->policy_details, - &dc->policy_details_serial_id, - &dc->policy_details.accumulated_total, - &dc->policy_details.fulfillment_state); - - if (qs < 0) - return qs; - } - qs = TEH_plugin->do_deposit ( - TEH_plugin->cls, - dc->deposit, - dc->known_coin_id, - &dc->h_payto, - (dc->has_policy) - ? &dc->policy_details_serial_id - : NULL, - &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) - { - /* FIXME #7267: conflicting contract != insufficient funds */ - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT, - &dc->deposit->coin.denom_pub_hash, - &dc->deposit->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &dc->deposit->coin.denom_pub_hash, - &dc->deposit->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT]++; - return qs; -} - - -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 TALER_ExtensionPolicyHashP *ph_policy = NULL; - bool no_policy_json; - 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_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &deposit.coin.h_age_commitment), - &deposit.coin.no_age_commitment), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &deposit.csig), - GNUNET_JSON_spec_timestamp ("timestamp", - &deposit.timestamp), - /* TODO: refund_deadline and merchant_pub will move into the - * extension policy_merchant_refunds */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("refund_deadline", - &deposit.refund_deadline), - NULL), - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &deposit.wire_deadline), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("policy", - &dc.policy_details.policy_json), - &no_policy_json), - GNUNET_JSON_spec_end () - }; - struct TALER_MerchantWireHashP 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 */ - } - } - - dc.has_policy = ! no_policy_json; - - /* 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); - } - if (GNUNET_TIME_absolute_is_never (deposit.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); - } - 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 (0 > TALER_amount_cmp (&dk->meta.value, - &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_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE, - NULL); - } - 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"); - } - if (dk->denom_pub.cipher != deposit.coin.denom_sig.cipher) - { - /* denomination cipher and denomination signature cipher not the same */ - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL); - } - - deposit.deposit_fee = dk->meta.fees.deposit; - /* check coin signature */ - switch (dk->denom_pub.cipher) - { - case TALER_DENOMINATION_RSA: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; - break; - case TALER_DENOMINATION_CS: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; - break; - default: - break; - } - 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_FORBIDDEN, - 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); - } - - /* Check policy input and create policy details */ - if (dc.has_policy) - { - const char *error_hint = NULL; - - if (GNUNET_OK != - TALER_extensions_create_policy_details ( - dc.policy_details.policy_json, - &dc.policy_details, - &error_hint)) - 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_details.policy_json, - &dc.h_policy); - ph_policy = &dc.h_policy; - } - - 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.coin.h_age_commitment, - ph_policy, - &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_FORBIDDEN, - 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"); - } - - /* execute transaction */ - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "execute deposit", - TEH_MT_REQUEST_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, - ph_policy, - &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_deposit.h b/src/exchange/taler-exchange-httpd_deposit.h deleted file mode 100644 index a4d598a69..000000000 --- a/src/exchange/taler-exchange-httpd_deposit.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014 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.h - * @brief Handle /deposit requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_DEPOSIT_H -#define TALER_EXCHANGE_HTTPD_DEPOSIT_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * 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); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_deposits_get.c b/src/exchange/taler-exchange-httpd_deposits_get.c index ebbb13e08..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" @@ -38,6 +39,26 @@ struct DepositWtidContext { /** + * 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; + + /** + * 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; @@ -65,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; @@ -80,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). */ @@ -101,10 +133,47 @@ struct DepositWtidContext * 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. * @@ -228,33 +297,101 @@ 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 ctx context of the signed request to execute * @return MHD result code */ static MHD_RESULT handle_track_transaction_request ( - struct MHD_Connection *connection, struct DepositWtidContext *ctx) { - 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; + 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 (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, @@ -270,94 +407,118 @@ handle_track_transaction_request ( ctx->kyc.ok), GNUNET_JSON_pack_timestamp ("execution_time", ctx->execution_time)); + } return reply_deposit_details (connection, 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); +} + + 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 DepositWtidContext ctx; + struct DepositWtidContext *ctx = rc->rh_ctx; - 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]); - } - 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 */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (NULL == ctx) { + 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 != - TALER_merchant_deposit_verify (&ctx.merchant, - &ctx.coin_pub, - &ctx.h_contract_terms, - &ctx.h_wire, - &merchant_sig)) + 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_FORBIDDEN, - TALER_EC_EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID, - NULL); + 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, - &ctx); + 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 48c3f4a94..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 @@ -23,6 +23,7 @@ #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" @@ -148,7 +149,7 @@ extension_update_event_cb (void *cls, TEH_age_restriction_enabled = false; if (NULL != conf) { - TEH_age_restriction_enabled = true; + 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", @@ -208,16 +209,20 @@ TEH_extensions_init () { const struct TALER_Extension *ext = it->extension; uint32_t typ = htonl (ext->type); - char *manifest = json_dumps (ext->manifest (ext), JSON_COMPACT); + 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)); - free (manifest); } return GNUNET_OK; @@ -252,11 +257,16 @@ policy_fulfillment_transaction ( { 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, @@ -334,14 +344,48 @@ TEH_extensions_post_handler ( qs = TEH_plugin->get_policy_details (TEH_plugin->cls, &hcs[idx], &policy_details[idx]); - if (qs < 0) + 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) { - error_msg = "a policy_hash_code couldn't be found"; + 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); @@ -353,44 +397,39 @@ TEH_extensions_post_handler ( } + if (GNUNET_OK != + ext->policy_post_handler (root, + &args[1], + policy_details, + policy_details_count, + &output)) { - enum GNUNET_GenericReturnValue ret; - - ret = ext->policy_post_handler (root, - &args[1], - policy_details, - policy_details_count, - &output); - - if (GNUNET_OK != ret) - { - TALER_MHD_reply_json_steal ( - rc->connection, - output, - MHD_HTTP_BAD_REQUEST); - } + return TALER_MHD_reply_json_steal ( + rc->connection, + output, + MHD_HTTP_BAD_REQUEST); + } - /* execute fulfillment transaction */ + /* 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)) { - 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; - } + json_decref (output); + return mhd_ret; } } diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c index d73283509..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-2022 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 @@ -388,6 +388,113 @@ struct SuspendedKeysRequests struct GNUNET_TIME_Absolute timeout; }; + +/** + * 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. */ @@ -466,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 @@ -565,12 +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)); - // nothing to do for TALER_DENOMINATION_CS - 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; } @@ -801,7 +1359,7 @@ 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}; enum GNUNET_GenericReturnValue ret; @@ -851,7 +1409,7 @@ load_age_mask (const char*section_name) * @param validity_duration how long does the key remain available for signing; * zero if the key has been revoked or purged * @param h_rsa 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 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. @@ -863,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) { @@ -893,10 +1451,9 @@ helper_rsa_cb ( 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, @@ -932,7 +1489,7 @@ helper_rsa_cb ( * @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 denom_pub the public key itself, NULL if the key was revoked or 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. @@ -944,7 +1501,7 @@ helper_cs_cb ( struct GNUNET_TIME_Timestamp start_time, struct GNUNET_TIME_Relative validity_duration, const struct TALER_CsPubHashP *h_cs, - const struct TALER_DenominationPublicKey *denom_pub, + struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub, const struct TALER_SecurityModulePublicKeyP *sm_pub, const struct TALER_SecurityModuleSignatureP *sm_sig) { @@ -974,9 +1531,9 @@ helper_cs_cb ( hd->validity_duration = validity_duration; hd->h_details.h_cs = *h_cs; hd->sm_sig = *sm_sig; - GNUNET_assert (TALER_DENOMINATION_CS == denom_pub->cipher); - TALER_denom_pub_deep_copy (&hd->denom_pub, - denom_pub); + 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, @@ -1082,6 +1639,7 @@ setup_key_helpers (struct HelperState *hs) = GNUNET_CONTAINER_multipeermap_create (32, GNUNET_NO /* MUST BE NO! */); hs->rsadh = TALER_CRYPTO_helper_rsa_connect (TEH_cfg, + "taler-exchange", &helper_rsa_cb, hs); if (NULL == hs->rsadh) @@ -1090,6 +1648,7 @@ setup_key_helpers (struct HelperState *hs) return GNUNET_SYSERR; } hs->csdh = TALER_CRYPTO_helper_cs_connect (TEH_cfg, + "taler-exchange", &helper_cs_cb, hs); if (NULL == hs->csdh) @@ -1098,6 +1657,7 @@ setup_key_helpers (struct HelperState *hs) return GNUNET_SYSERR; } hs->esh = TALER_CRYPTO_helper_esign_connect (TEH_cfg, + "taler-exchange", &helper_esign_cb, hs); if (NULL == hs->esh) @@ -1338,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) || @@ -1350,8 +1928,8 @@ 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; @@ -1364,7 +1942,6 @@ denomination_info_cb ( &dk->h_denom_pub.hash, dk, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - } @@ -1387,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; @@ -1668,14 +2258,14 @@ add_denom_key_cb (void *cls, /** * 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); @@ -1683,27 +2273,38 @@ setup_general_response_headers (struct TEH_KeyStateHandle *ksh, MHD_add_response_header (response, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json")); - TALER_MHD_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); + /* 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, @@ -1717,12 +2318,6 @@ setup_general_response_headers (struct TEH_KeyStateHandle *ksh, MHD_add_response_header (response, MHD_HTTP_HEADER_VARY, MHD_HTTP_HEADER_ACCEPT_ENCODING)); - /* Information is always public, revalidate after 1 hour */ - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_CACHE_CONTROL, - "public,max-age=3600")); - return GNUNET_OK; } @@ -1753,44 +2348,45 @@ wallet_threshold_cb (void *cls, * * @param[in,out] ksh key state handle we build @a krd for * @param[in] denom_keys_hash hash over all the denomination 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 grouped_denominations list of grouped denominations to return - * @param[in] h_grouped XOR of all hashes in @a grouped_demoninations + * @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 *grouped_denominations, - const struct GNUNET_HashCode *h_grouped) + json_t *grouped_denominations) { struct KeysResponseData krd; struct TALER_ExchangePublicKeyP exchange_pub; struct TALER_ExchangeSignatureP exchange_sig; - struct TALER_ExchangePublicKeyP grouped_exchange_pub; - struct TALER_ExchangeSignatureP grouped_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 != h_grouped); 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)); + GNUNET_TIME_timestamp2s (last_cherry_pick_date)); - /* Sign hash over denomination keys */ + /* Sign hash over master signatures of all denomination keys until this time + (in reverse order). */ { enum TALER_ErrorCode ec; @@ -1799,7 +2395,7 @@ create_krd (struct TEH_KeyStateHandle *ksh, TALER_exchange_online_key_set_sign ( &TEH_keys_exchange_sign2_, ksh, - last_cpd, + last_cherry_pick_date, denom_keys_hash, &exchange_pub, &exchange_sig))) @@ -1811,33 +2407,6 @@ create_krd (struct TEH_KeyStateHandle *ksh, } } - /* Sign grouped hash */ - { - enum TALER_ErrorCode ec; - - if (TALER_EC_NONE != - (ec = - TALER_exchange_online_key_set_sign ( - &TEH_keys_exchange_sign2_, - ksh, - last_cpd, - h_grouped, - &grouped_exchange_pub, - &grouped_exchange_sig))) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Could not create key response data: cannot sign grouped hash (%s)\n", - TALER_ErrorCode_get_hint (ec)); - return GNUNET_SYSERR; - } - } - - /* both public keys really must be the same */ - GNUNET_assert (0 == - memcmp (&grouped_exchange_pub, - &exchange_pub, - sizeof(exchange_pub))); - { const struct SigningKey *sk; @@ -1848,6 +2417,12 @@ 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), @@ -1855,8 +2430,20 @@ create_krd (struct TEH_KeyStateHandle *ksh, 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", @@ -1865,8 +2452,15 @@ 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", @@ -1874,13 +2468,11 @@ create_krd (struct TEH_KeyStateHandle *ksh, 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", - &exchange_sig), - GNUNET_JSON_pack_data_auto ("denominations_sig", - &grouped_exchange_sig)); + GNUNET_JSON_pack_data_auto ("exchange_sig", + &exchange_sig)); GNUNET_assert (NULL != keys); /* Set wallet limit if KYC is configured */ @@ -2001,9 +2593,9 @@ create_krd (struct TEH_KeyStateHandle *ksh, 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, @@ -2023,16 +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); @@ -2041,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 @@ -2052,23 +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; + struct SignKeyCtx sctx = { + .min_sk_frequency = GNUNET_TIME_UNIT_FOREVER_REL + }; json_t *grouped_denominations = NULL; - struct GNUNET_TIME_Timestamp last_cpd; + struct GNUNET_TIME_Timestamp last_cherry_pick_date; struct GNUNET_CONTAINER_Heap *heap; - struct GNUNET_HashContext *hash_context = NULL; - struct GNUNET_HashCode grouped_hash_xor = {0}; + 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 = { @@ -2085,121 +2892,52 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) sctx.min_sk_frequency); } - denoms = json_array (); - GNUNET_assert (NULL != denoms); - hash_context = GNUNET_CRYPTO_hash_context_start (); - - grouped_denominations = json_array (); - GNUNET_assert (NULL != grouped_denominations); + last_cherry_pick_date = GNUNET_TIME_UNIT_ZERO_TS; - last_cpd = GNUNET_TIME_UNIT_ZERO_TS; - - // FIXME: This block contains the implementation of the DEPRECATED - // "denom_pubs" array along with the new grouped "denominations". - // "denom_pubs" Will be removed sooner or later. { struct TEH_DenominationKey *dk; struct GNUNET_CONTAINER_MultiHashMap *denominations_by_group; - /* 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; - - /** - * xor of all hashes of denominations in that group - */ - struct GNUNET_HashCode hash_xor; - }; denominations_by_group = GNUNET_CONTAINER_multihashmap_create (1024, GNUNET_NO /* NO, because keys are only on the stack */); - - - /* heap = min heap, sorted by start time */ + /* 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_cpd != + * 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, - grouped_denominations, - &grouped_hash_xor)) + 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); - json_decref (grouped_denominations); - json_decref (sctx.signkeys); - json_decref (recoup); - return GNUNET_SYSERR; + goto CLEANUP; } } - last_cpd = dk->meta.start; - - { - json_t *denom; - - denom = - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("master_sig", - &dk->master_sig), - GNUNET_JSON_pack_timestamp ("stamp_start", - dk->meta.start), - GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", - dk->meta.expire_withdraw), - GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", - 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_DENOM_FEES ("fee", - &dk->meta.fees)); - - GNUNET_CRYPTO_hash_context_read (hash_context, - &dk->h_denom_pub, - sizeof (struct GNUNET_HashCode)); - - GNUNET_assert ( - 0 == - json_array_append_new ( - denoms, - denom)); - - } - + last_cherry_pick_date = dk->meta.start; /* * Group the denominations by {cipher, value, fees, age_mask}. * @@ -2208,70 +2946,70 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) * denominations_by_group. */ { - static const char *denoms_key = "denoms"; - struct groupData *group; - json_t *list; + struct GroupData *group; json_t *entry; struct GNUNET_HashCode key; struct TALER_DenominationGroup meta = { - .cipher = dk->denom_pub.cipher, + .cipher = dk->denom_pub.bsign_pub_key->cipher, .value = dk->meta.value, .fees = dk->meta.fees, .age_mask = dk->meta.age_mask, }; - memset (&meta.hash, 0, sizeof(meta.hash)); - /* Search the group/JSON-blob for the key */ - GNUNET_CRYPTO_hash (&meta, sizeof(meta), &key); - - group = - (struct groupData *) GNUNET_CONTAINER_multihashmap_get ( - denominations_by_group, - &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; - char *cipher; - - group = GNUNET_new (struct groupData); - memset (group, 0, sizeof(*group)); + const char *cipher; + group = GNUNET_new (struct GroupData); switch (meta.cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: cipher = age_restricted ? "RSA+age_restricted" : "RSA"; break; - case TALER_DENOMINATION_CS: + 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), - TALER_JSON_PACK_DENOM_FEES ("fee", &meta.fees), - TALER_JSON_pack_amount ("value", &meta.value)); + 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) { - int r = json_object_set_new (group->json, - "age_mask", - json_integer (meta.age_mask.bits)); - GNUNET_assert (0 == r); + 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; } - - /* Create a new array for the denominations in this group */ - list = json_array (); - GNUNET_assert (NULL != list); + group->group_off + = json_array_size (grouped_denominations); GNUNET_assert (0 == - json_object_set_new (group->json, - denoms_key, - list)); + json_array_append_new ( + grouped_denominations, + group->json)); GNUNET_assert ( GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (denominations_by_group, @@ -2283,23 +3021,32 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) /* 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 TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: key_spec = - GNUNET_JSON_pack_rsa_public_key ("rsa_pub", - dk->denom_pub.details. - rsa_public_key); + GNUNET_JSON_pack_rsa_public_key ( + "rsa_pub", + dk->denom_pub.bsign_pub_key->details.rsa_public_key); break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: key_spec = - GNUNET_JSON_pack_data_varsize ("cs_pub", - &dk->denom_pub.details. - cs_public_key, - sizeof (dk->denom_pub.details. - cs_public_key)); + 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); @@ -2308,6 +3055,12 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) 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", @@ -2321,106 +3074,80 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) GNUNET_assert (NULL != entry); } - /* Build up the running xor of all hashes of the denominations in this - group */ - GNUNET_CRYPTO_hash_xor (&dk->h_denom_pub.hash, - &group->hash_xor, - &group->hash_xor); - + /* 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 */ - list = json_object_get (group->json, denoms_key); - GNUNET_assert (NULL != list); - GNUNET_assert (true == json_is_array (list)); + GNUNET_assert (json_is_array (group->list)); GNUNET_assert (0 == - json_array_append_new (list, entry)); + json_array_append_new (group->list, + entry)); } } /* loop over heap ends */ - /* Create the JSON-array of grouped denominations */ - if (0 < - GNUNET_CONTAINER_multihashmap_size (denominations_by_group)) - { - struct GNUNET_CONTAINER_MultiHashMapIterator *iter; - struct groupData *group = NULL; - - iter = - GNUNET_CONTAINER_multihashmap_iterator_create (denominations_by_group); - - while (GNUNET_OK == - GNUNET_CONTAINER_multihashmap_iterator_next (iter, - NULL, - (const - void **) &group)) - { - /* Add the XOR over all hashes of denominations in this group to the group */ - GNUNET_assert (0 == - json_object_set_new ( - group->json, - "hash", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto (NULL, - &group->hash_xor)))); - - /* Add this group to the array */ - GNUNET_assert (0 == - json_array_append_new ( - grouped_denominations, - group->json)); - - /* Build the running XOR over all hash(_xor) */ - GNUNET_CRYPTO_hash_xor (&group->hash_xor, - &grouped_hash_xor, - &grouped_hash_xor); - - GNUNET_free (group); - } - - GNUNET_CONTAINER_multihashmap_iterator_destroy (iter); - GNUNET_CONTAINER_multihashmap_destroy (denominations_by_group); - - } + 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, - grouped_denominations, - &grouped_hash_xor)) + 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); - json_decref (grouped_denominations); - 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); } + ret = GNUNET_OK; + +CLEANUP: + GNUNET_array_grow (sig_ctx.elements, + sig_ctx.elements_size, + 0); json_decref (grouped_denominations); - json_decref (sctx.signkeys); + if (NULL != sctx.signkeys) + json_decref (sctx.signkeys); json_decref (recoup); - json_decref (denoms); - return GNUNET_OK; + return ret; } @@ -2451,6 +3178,21 @@ global_fee_info_cb ( 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); @@ -2523,9 +3265,9 @@ 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! */ @@ -2601,7 +3343,7 @@ build_key_state (struct HelperState *hs, 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; @@ -2647,7 +3389,7 @@ keys_get_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); @@ -2730,16 +3472,16 @@ TEH_keys_denomination_by_hash ( 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, +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) @@ -2761,66 +3503,11 @@ TEH_keys_denomination_by_hash2 ( enum TALER_ErrorCode -TEH_keys_denomination_sign ( - const struct TEH_CoinSignData *csd, - bool for_melt, - struct TALER_BlindedDenominationSignature *bs) -{ - struct TEH_KeyStateHandle *ksh; - struct HelperDenomination *hd; - const struct TALER_DenominationHashP *h_denom_pub = csd->h_denom_pub; - const struct TALER_BlindedPlanchet *bp = csd->bp; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; - 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->cipher != hd->denom_pub.cipher) - return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - switch (hd->denom_pub.cipher) - { - case TALER_DENOMINATION_RSA: - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA]++; - { - struct TALER_CRYPTO_RsaSignRequest rsr = { - .h_rsa = &hd->h_details.h_rsa, - .msg = bp->details.rsa_blinded_planchet.blinded_msg, - .msg_size = bp->details.rsa_blinded_planchet.blinded_msg_size - }; - - return TALER_CRYPTO_helper_rsa_sign ( - ksh->helpers->rsadh, - &rsr, - bs); - } - case TALER_DENOMINATION_CS: - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_CS]++; - { - struct TALER_CRYPTO_CsSignRequest csr; - - csr.h_cs = &hd->h_details.h_cs; - csr.blinded_planchet = &bp->details.cs_blinded_planchet; - return TALER_CRYPTO_helper_cs_sign ( - ksh->helpers->csdh, - &csr, - for_melt, - bs); - } - default: - return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - } -} - - -enum TALER_ErrorCode TEH_keys_denomination_batch_sign ( - const struct TEH_CoinSignData *csds, unsigned int csds_length, + const struct TEH_CoinSignData csds[static csds_length], bool for_melt, - struct TALER_BlindedDenominationSignature *bss) + struct TALER_BlindedDenominationSignature bss[static csds_length]) { struct TEH_KeyStateHandle *ksh; struct HelperDenomination *hd; @@ -2844,21 +3531,23 @@ TEH_keys_denomination_batch_sign ( &h_denom_pub->hash); if (NULL == hd) return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; - if (bp->cipher != hd->denom_pub.cipher) + if (bp->blinded_message->cipher != + hd->denom_pub.bsign_pub_key->cipher) return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - switch (hd->denom_pub.cipher) + switch (hd->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: rsrs[rsrs_pos].h_rsa = &hd->h_details.h_rsa; rsrs[rsrs_pos].msg - = bp->details.rsa_blinded_planchet.blinded_msg; + = bp->blinded_message->details.rsa_blinded_message.blinded_msg; rsrs[rsrs_pos].msg_size - = bp->details.rsa_blinded_planchet.blinded_msg_size; + = bp->blinded_message->details.rsa_blinded_message.blinded_msg_size; rsrs_pos++; break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: csrs[csrs_pos].h_cs = &hd->h_details.h_cs; - csrs[csrs_pos].blinded_planchet = &bp->details.cs_blinded_planchet; + csrs[csrs_pos].blinded_planchet + = &bp->blinded_message->details.cs_blinded_message; csrs_pos++; break; default: @@ -2881,8 +3570,8 @@ TEH_keys_denomination_batch_sign ( { ec = TALER_CRYPTO_helper_cs_batch_sign ( ksh->helpers->csdh, - csrs, csrs_pos, + csrs, for_melt, (0 == rsrs_pos) ? bss : cs); if (TALER_EC_NONE != ec) @@ -2897,8 +3586,8 @@ TEH_keys_denomination_batch_sign ( { ec = TALER_CRYPTO_helper_rsa_batch_sign ( ksh->helpers->rsadh, - rsrs, rsrs_pos, + rsrs, (0 == csrs_pos) ? bss : rs); if (TALER_EC_NONE != ec) { @@ -2920,12 +3609,12 @@ TEH_keys_denomination_batch_sign ( { const struct TALER_BlindedPlanchet *bp = csds[i].bp; - switch (bp->cipher) + switch (bp->blinded_message->cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: bss[i] = rs[rsrs_pos++]; break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: bss[i] = cs[csrs_pos++]; break; default: @@ -2941,10 +3630,10 @@ enum TALER_ErrorCode TEH_keys_denomination_cs_r_pub ( const struct TEH_CsDeriveData *cdd, bool for_melt, - struct TALER_DenominationCSPublicRPairP *r_pub) + struct GNUNET_CRYPTO_CSPublicRPairP *r_pub) { const struct TALER_DenominationHashP *h_denom_pub = cdd->h_denom_pub; - const struct TALER_CsNonce *nonce = cdd->nonce; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdd->nonce; struct TEH_KeyStateHandle *ksh; struct HelperDenomination *hd; @@ -2959,7 +3648,8 @@ TEH_keys_denomination_cs_r_pub ( { return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; } - if (TALER_DENOMINATION_CS != hd->denom_pub.cipher) + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) { return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; } @@ -2979,10 +3669,10 @@ TEH_keys_denomination_cs_r_pub ( enum TALER_ErrorCode TEH_keys_denomination_cs_batch_r_pub ( - const struct TEH_CsDeriveData *cdds, unsigned int cdds_length, + const struct TEH_CsDeriveData cdds[static cdds_length], bool for_melt, - struct TALER_DenominationCSPublicRPairP *r_pubs) + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]) { struct TEH_KeyStateHandle *ksh; struct HelperDenomination *hd; @@ -2996,7 +3686,7 @@ TEH_keys_denomination_cs_batch_r_pub ( for (unsigned int i = 0; i<cdds_length; i++) { const struct TALER_DenominationHashP *h_denom_pub = cdds[i].h_denom_pub; - const struct TALER_CsNonce *nonce = cdds[i].nonce; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdds[i].nonce; hd = GNUNET_CONTAINER_multihashmap_get (ksh->helpers->denom_keys, &h_denom_pub->hash); @@ -3004,7 +3694,8 @@ TEH_keys_denomination_cs_batch_r_pub ( { return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; } - if (TALER_DENOMINATION_CS != hd->denom_pub.cipher) + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) { return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; } @@ -3013,8 +3704,8 @@ TEH_keys_denomination_cs_batch_r_pub ( } return TALER_CRYPTO_helper_cs_r_batch_derive (ksh->helpers->csdh, - cdrs, cdds_length, + cdrs, for_melt, r_pubs); } @@ -3039,22 +3730,23 @@ TEH_keys_denomination_revoke (const struct TALER_DenominationHashP *h_denom_pub) GNUNET_break (0); return; } - switch (hd->denom_pub.cipher) + switch (hd->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: + 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; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: TALER_CRYPTO_helper_cs_revoke (ksh->helpers->csdh, &hd->h_details.h_cs); TEH_keys_update_states (); return; - default: - GNUNET_break (0); - return; } + GNUNET_break (0); + return; } @@ -3264,28 +3956,11 @@ TEH_keys_get_handler (struct TEH_RequestContext *rc, if ( (NULL != etag) && (0 == strcmp (etag, krd->etag)) ) - { - MHD_RESULT ret; - struct MHD_Response *resp; - - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp); - GNUNET_break (GNUNET_OK == - setup_general_response_headers (ksh, - resp)); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - krd->etag)); - ret = MHD_queue_response (rc->connection, - MHD_HTTP_NOT_MODIFIED, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } + 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 == @@ -3401,9 +4076,10 @@ TEH_keys_load_fees (struct TEH_KeyStateHandle *ksh, 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 { @@ -3436,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 ( diff --git a/src/exchange/taler-exchange-httpd_keys.h b/src/exchange/taler-exchange-httpd_keys.h index 8758afb71..e526385ff 100644 --- a/src/exchange/taler-exchange-httpd_keys.h +++ b/src/exchange/taler-exchange-httpd_keys.h @@ -154,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 @@ -234,8 +276,8 @@ TEH_keys_denomination_by_hash ( * or NULL if @a h_denom_pub could not be found */ struct TEH_DenominationKey * -TEH_keys_denomination_by_hash2 ( - struct TEH_KeyStateHandle *ksh, +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); @@ -258,22 +300,7 @@ struct TEH_CoinSignData /** - * Request to sign @a csd for melting. - * - * @param csd identifies data to blindly sign and key to sign with - * @param for_melt true if this is for a melt operation - * @param[out] bs set to the blind signature on success - * @return #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TEH_keys_denomination_sign ( - const struct TEH_CoinSignData *csd, - bool for_melt, - struct TALER_BlindedDenominationSignature *bs); - - -/** - * Request to sign @a csds for melting. + * Request to sign @a csds. * * @param csds array with data to blindly sign (and keys to sign with) * @param csds_length length of @a csds array @@ -283,10 +310,10 @@ TEH_keys_denomination_sign ( */ enum TALER_ErrorCode TEH_keys_denomination_batch_sign ( - const struct TEH_CoinSignData *csds, unsigned int csds_length, + const struct TEH_CoinSignData csds[static csds_length], bool for_melt, - struct TALER_BlindedDenominationSignature *bss); + struct TALER_BlindedDenominationSignature bss[static csds_length]); /** @@ -302,7 +329,7 @@ struct TEH_CsDeriveData /** * Nonce to use. */ - const struct TALER_CsNonce *nonce; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce; }; @@ -318,7 +345,7 @@ enum TALER_ErrorCode TEH_keys_denomination_cs_r_pub ( const struct TEH_CsDeriveData *cdd, bool for_melt, - struct TALER_DenominationCSPublicRPairP *r_pub); + struct GNUNET_CRYPTO_CSPublicRPairP *r_pub); /** @@ -333,10 +360,10 @@ TEH_keys_denomination_cs_r_pub ( */ enum TALER_ErrorCode TEH_keys_denomination_cs_batch_r_pub ( - const struct TEH_CsDeriveData *cdds, unsigned int cdds_length, + const struct TEH_CsDeriveData cdds[static cdds_length], bool for_melt, - struct TALER_DenominationCSPublicRPairP *r_pubs); + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]); /** @@ -363,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? diff --git a/src/exchange/taler-exchange-httpd_kyc-check.c b/src/exchange/taler-exchange-httpd_kyc-check.c index c88859268..362c20a2e 100644 --- a/src/exchange/taler-exchange-httpd_kyc-check.c +++ b/src/exchange/taler-exchange-httpd_kyc-check.c @@ -113,6 +113,11 @@ struct KycPoller 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; @@ -233,7 +238,8 @@ initiate_cb ( kyp->ih = NULL; kyp->ih_done = true; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC initiation completed with ec=%d (%s)\n", + "KYC initiation `%s' completed with ec=%d (%s)\n", + provider_legitimization_id, ec, (TALER_EC_NONE == ec) ? redirect_url @@ -254,8 +260,9 @@ initiate_cb ( &kyp->h_payto, provider_user_id, provider_legitimization_id, + redirect_url, GNUNET_TIME_UNIT_ZERO_ABS); - if (qs < 0) + 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), @@ -297,12 +304,14 @@ kyc_check (void *cls, 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) { @@ -381,14 +390,11 @@ kyc_check (void *cls, if (kyp->ih_done) return qs; - - qs = TEH_plugin->insert_kyc_requirement_process ( + qs = TEH_plugin->get_pending_kyc_requirement_process ( TEH_plugin->cls, &h_payto, kyp->section_name, - NULL, - NULL, - &kyp->process_row); + &redirect_url); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -400,6 +406,34 @@ kyc_check (void *cls, "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); @@ -514,34 +548,19 @@ TEH_handler_kyc_check ( "usertype"); } - { - 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)); - } - } + 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_ACCEPTED, + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status), + GNUNET_JSON_pack_string ("kyc_url", + kyp->kyc_url)); } if ( (NULL == kyp->eh) && @@ -576,10 +595,32 @@ TEH_handler_kyc_check ( "Transaction failed.\n"); return res; } + /* KYC plugin generated reply? */ + if (NULL != kyp->kyc_url) + { + 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", @@ -610,11 +651,12 @@ TEH_handler_kyc_check ( { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Suspending HTTP request on timeout (%s) now...\n", - GNUNET_TIME_relative2s (GNUNET_TIME_absolute_get_duration ( + 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); @@ -622,16 +664,6 @@ TEH_handler_kyc_check ( return MHD_YES; } - /* KYC plugin generated reply? */ - if (NULL != kyp->kyc_url) - { - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ("kyc_url", - kyp->kyc_url)); - } - if (TALER_EC_NONE != kyp->ec) { return TALER_MHD_reply_with_ec (rc->connection, @@ -665,6 +697,8 @@ 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", diff --git a/src/exchange/taler-exchange-httpd_kyc-proof.c b/src/exchange/taler-exchange-httpd_kyc-proof.c index 6d06f0c82..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-2022 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,11 +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" @@ -69,6 +70,11 @@ struct KycProofContext struct TALER_KYCLOGIC_ProofHandle *ph; /** + * KYC AML trigger operation. + */ + struct TEH_KycAmlTrigger *kat; + + /** * Process information about the user for the plugin from the database, can * be NULL. */ @@ -160,6 +166,101 @@ TEH_kyc_proof_cleanup (void) /** + * 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 + */ +static void +proof_finish ( + void *cls, + unsigned int http_status, + struct MHD_Response *response) +{ + struct KycProofContext *kpc = cls; + + kpc->kat = NULL; + kpc->response_code = http_status; + kpc->response = response; + kpc_resume (kpc); +} + + +/** + * Generate HTML error for @a connection using @a template. + * + * @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 + */ +struct MHD_Response * +make_html_error (struct MHD_Connection *connection, + const char *template, + unsigned int *http_status, + enum TALER_ErrorCode ec, + const char *message) +{ + 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; +} + + +/** + * Respond with an HTML message on the given @a rc. + * + * @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 MHD_RESULT +respond_html_ec (struct TEH_RequestContext *rc, + unsigned int http_status, + const char *template, + enum TALER_ErrorCode ec, + const char *message) +{ + 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; +} + + +/** * Function called with the result of a proof check operation. * * Note that the "decref" for the @a response @@ -192,74 +293,104 @@ proof_cb ( kpc->ph = NULL; GNUNET_async_scope_enter (&rc->async_scope_id, &old_scope); - - if (TALER_KYCLOGIC_STATUS_SUCCESS == status) + switch (status) { - enum GNUNET_DB_QueryStatus qs; - size_t eas; - void *ea; - const char *birthdate; - struct GNUNET_ShortHashCode kyc_prox; - - TALER_CRYPTO_attributes_to_kyc_prox (attributes, - &kyc_prox); - birthdate = json_string_value (json_object_get (attributes, - TALER_ATTRIBUTE_BIRTHDATE)); - TALER_CRYPTO_kyc_attributes_encrypt (&TEH_attribute_key, - attributes, - &ea, - &eas); - qs = TEH_plugin->insert_kyc_attributes ( - TEH_plugin->cls, - &kpc->h_payto, - &kyc_prox, - kpc->provider_section, - birthdate, - GNUNET_TIME_timestamp_get (), - GNUNET_TIME_absolute_to_timestamp (expiration), - eas, - ea); - GNUNET_free (ea); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) + 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) { - GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; if (NULL != response) MHD_destroy_response (response); - kpc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - kpc->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_kyc_attributes"); - GNUNET_async_scope_restore (&old_scope); - return; + response = make_html_error (kpc->rc->connection, + "kyc-proof-internal-error", + &http_status, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + "[exchange] AML_KYC_TRIGGER"); } - qs = TEH_plugin->update_kyc_process_by_row (TEH_plugin->cls, - kpc->process_row, - kpc->provider_section, - &kpc->h_payto, - provider_user_id, - provider_legitimization_id, - expiration); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) + 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) { - GNUNET_break (0); + char *msg; + + /* OAuth2 server had a problem, do NOT log this as a KYC failure */ if (NULL != response) MHD_destroy_response (response); - kpc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - kpc->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "set_kyc_ok"); - GNUNET_async_scope_restore (&old_scope); - return; + 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)) + { + 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"); + } } + 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) kpc->process_row, + (int) status); + break; } - else + 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); } - kpc->response_code = http_status; - kpc->response = response; - kpc_resume (kpc); GNUNET_async_scope_restore (&old_scope); } @@ -279,6 +410,11 @@ clean_kpc (struct TEH_RequestContext *rc) kpc->logic->proof_cancel (kpc->ph); kpc->ph = NULL; } + if (NULL != kpc->kat) + { + TEH_kyc_finished_cancel (kpc->kat); + kpc->kat = NULL; + } if (NULL != kpc->response) { MHD_destroy_response (kpc->response); @@ -297,7 +433,6 @@ TEH_handler_kyc_proof ( { struct KycProofContext *kpc = rc->rh_ctx; const char *provider_section_or_logic = args[0]; - const char *h_payto; if (NULL == kpc) { @@ -305,38 +440,19 @@ TEH_handler_kyc_proof ( if (NULL == provider_section_or_logic) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - "'/kyc-proof/$PROVIDER_SECTION?state=$H_PAYTO' required"); - } - h_payto = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, - "state"); - if (NULL == h_payto) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "h_payto"); + 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 (GNUNET_OK != - GNUNET_STRINGS_string_to_data (h_payto, - strlen (h_payto), - &kpc->h_payto, - sizeof (kpc->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"); - } + 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, @@ -344,10 +460,11 @@ TEH_handler_kyc_proof ( &kpc->provider_section)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, - provider_section_or_logic); + return respond_html_ec (rc, + MHD_HTTP_NOT_FOUND, + "kyc-proof-target-unknown", + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + provider_section_or_logic); } if (NULL != kpc->provider_section) { @@ -358,10 +475,11 @@ TEH_handler_kyc_proof ( kpc->provider_section)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "PROVIDER_SECTION"); + return respond_html_ec (rc, + MHD_HTTP_BAD_REQUEST, + "kyc-proof-bad-request", + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "PROVIDER_SECTION"); } qs = TEH_plugin->lookup_kyc_process_by_account ( @@ -376,26 +494,28 @@ TEH_handler_kyc_proof ( { case GNUNET_DB_STATUS_HARD_ERROR: case GNUNET_DB_STATUS_SOFT_ERROR: - return TALER_MHD_reply_with_ec (rc->connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "lookup_kyc_requirement_by_account"); + 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 TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, - kpc->provider_section); + 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; } if (GNUNET_TIME_absolute_is_future (expiration)) { /* KYC not required */ - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + return respond_html_ec (rc, + MHD_HTTP_OK, + "kyc-proof-already-done", + TALER_EC_NONE, + NULL); } } kpc->ph = kpc->logic->proof (kpc->logic->cls, @@ -410,10 +530,11 @@ TEH_handler_kyc_proof ( if (NULL == kpc->ph) { GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "could not start proof with KYC logic"); + 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"); } @@ -428,10 +549,11 @@ TEH_handler_kyc_proof ( if (NULL == kpc->response) { GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "handler resumed without response"); + 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 */ diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.c b/src/exchange/taler-exchange-httpd_kyc-wallet.c index 77f2dea78..21d07422d 100644 --- a/src/exchange/taler-exchange-httpd_kyc-wallet.c +++ b/src/exchange/taler-exchange-httpd_kyc-wallet.c @@ -42,6 +42,11 @@ struct KycRequestContext struct TALER_PaytoHashP h_payto; /** + * The reserve's public key + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** * KYC status, with row with the legitimization requirement. */ struct TALER_EXCHANGEDB_KycStatus kyc; @@ -141,6 +146,7 @@ wallet_kyc_check (void *cls, 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) { @@ -170,12 +176,11 @@ TEH_handler_kyc_wallet ( { struct TALER_ReserveSignatureP reserve_sig; struct KycRequestContext krc; - struct TALER_ReservePublicKeyP reserve_pub; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("reserve_sig", &reserve_sig), GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &reserve_pub), + &krc.reserve_pub), TALER_JSON_spec_amount ("balance", TEH_currency, &krc.balance), @@ -195,7 +200,7 @@ TEH_handler_kyc_wallet ( TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != - TALER_wallet_account_setup_verify (&reserve_pub, + TALER_wallet_account_setup_verify (&krc.reserve_pub, &krc.balance, &reserve_sig)) { @@ -210,7 +215,7 @@ TEH_handler_kyc_wallet ( char *payto_uri; payto_uri = TALER_reserve_make_payto (TEH_base_url, - &reserve_pub); + &krc.reserve_pub); TALER_payto_hash (payto_uri, &krc.h_payto); GNUNET_log (GNUNET_ERROR_TYPE_INFO, diff --git a/src/exchange/taler-exchange-httpd_kyc-webhook.c b/src/exchange/taler-exchange-httpd_kyc-webhook.c index f8fe711da..b92b43e69 100644 --- a/src/exchange/taler-exchange-httpd_kyc-webhook.c +++ b/src/exchange/taler-exchange-httpd_kyc-webhook.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + 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 @@ -28,6 +28,7 @@ #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" @@ -54,6 +55,11 @@ struct KycWebhookContext 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; @@ -141,6 +147,28 @@ TEH_kyc_webhook_cleanup (void) /** + * 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 @@ -178,58 +206,53 @@ webhook_finished_cb ( switch (status) { case TALER_KYCLOGIC_STATUS_SUCCESS: - /* _successfully_ resumed case */ + 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)) { - enum GNUNET_DB_QueryStatus qs; - size_t eas; - void *ea; - const char *birthdate; - struct GNUNET_ShortHashCode kyc_prox; - - TALER_CRYPTO_attributes_to_kyc_prox (attributes, - &kyc_prox); - birthdate = json_string_value (json_object_get (attributes, - TALER_ATTRIBUTE_BIRTHDATE)); - TALER_CRYPTO_kyc_attributes_encrypt (&TEH_attribute_key, - attributes, - &ea, - &eas); - qs = TEH_plugin->insert_kyc_attributes ( - TEH_plugin->cls, - account_id, - &kyc_prox, - provider_section, - birthdate, - GNUNET_TIME_timestamp_get (), - GNUNET_TIME_absolute_to_timestamp (expiration), - eas, - ea); - GNUNET_free (ea); - if (qs < 0) - { - GNUNET_break (0); - kwh->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_kyc_attributes"); - kwh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - kwh_resume (kwh); - return; - } - qs = TEH_plugin->update_kyc_process_by_row (TEH_plugin->cls, - process_row, - provider_section, - account_id, - provider_user_id, - provider_legitimization_id, - expiration); - if (qs < 0) - { - GNUNET_break (0); - kwh->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "set_kyc_ok"); - kwh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - kwh_resume (kwh); - return; - } + 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: @@ -238,12 +261,13 @@ webhook_finished_cb ( provider_user_id, provider_legitimization_id, (unsigned long long) process_row, - status); + (int) status); break; } - kwh->response = response; - kwh->response_code = http_status; - kwh_resume (kwh); + GNUNET_break (NULL == kwh->kat); + kyc_aml_webhook_finished (kwh, + http_status, + response); } @@ -262,6 +286,11 @@ clean_kwh (struct TEH_RequestContext *rc) 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); @@ -311,6 +340,10 @@ handler_kyc_webhook_generic ( 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, @@ -336,13 +369,17 @@ handler_kyc_webhook_generic ( MHD_suspend_connection (rc->connection); return MHD_YES; } + GNUNET_break (GNUNET_NO == kwh->suspended); if (NULL != kwh->response) { - /* handle _failed_ resumed cases */ - return MHD_queue_response (rc->connection, - kwh->response_code, - 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 diff --git a/src/exchange/taler-exchange-httpd_link.c b/src/exchange/taler-exchange-httpd_link.c index 9b7e297bc..3d92a11a3 100644 --- a/src/exchange/taler-exchange-httpd_link.c +++ b/src/exchange/taler-exchange-httpd_link.c @@ -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. @@ -153,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) @@ -178,26 +178,13 @@ 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 != 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_auditors.c b/src/exchange/taler-exchange-httpd_management_auditors.c index 9c7a5c472..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), diff --git a/src/exchange/taler-exchange-httpd_management_drain.c b/src/exchange/taler-exchange-httpd_management_drain.c index 565c292f4..1e490d799 100644 --- a/src/exchange/taler-exchange-httpd_management_drain.c +++ b/src/exchange/taler-exchange-httpd_management_drain.c @@ -124,8 +124,8 @@ TEH_handler_management_post_drain ( struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("debit_account_section", &dc.account_section), - GNUNET_JSON_spec_string ("credit_payto_uri", - &dc.payto_uri), + 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", diff --git a/src/exchange/taler-exchange-httpd_management_extensions.c b/src/exchange/taler-exchange-httpd_management_extensions.c index d5841a3c4..3b24bace7 100644 --- a/src/exchange/taler-exchange-httpd_management_extensions.c +++ b/src/exchange/taler-exchange-httpd_management_extensions.c @@ -145,7 +145,7 @@ set_extensions (void *cls, static enum GNUNET_GenericReturnValue verify_extensions_from_json ( - json_t *extensions, + const json_t *extensions, struct SetExtensionsContext *sec) { const char*name; @@ -160,7 +160,7 @@ verify_extensions_from_json ( sec->extensions = GNUNET_new_array (sec->num_extensions, struct Extension); - json_object_foreach (extensions, name, manifest) + json_object_foreach ((json_t *) extensions, name, manifest) { int critical = 0; json_t *config; @@ -200,11 +200,11 @@ TEH_handler_management_post_extensions ( const json_t *root) { MHD_RESULT ret; - json_t *extensions; + const json_t *extensions; struct SetExtensionsContext sec = {0}; struct GNUNET_JSON_Specification top_spec[] = { - GNUNET_JSON_spec_json ("extensions", - &extensions), + GNUNET_JSON_spec_object_const ("extensions", + &extensions), GNUNET_JSON_spec_fixed_auto ("extensions_sig", &sec.extensions_sig), GNUNET_JSON_spec_end () @@ -223,31 +223,19 @@ TEH_handler_management_post_extensions ( return MHD_YES; /* failure */ } - /* Ensure we have an object */ - if ((! json_is_object (extensions)) && - (! json_is_null (extensions))) - { - GNUNET_JSON_parse_free (top_spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "invalid object"); - } - /* Verify the signature */ { struct TALER_ExtensionManifestsHashP h_manifests; if (GNUNET_OK != - TALER_JSON_extensions_manifests_hash (extensions, &h_manifests) || + 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_sig)) { - GNUNET_JSON_parse_free (top_spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, @@ -263,7 +251,6 @@ TEH_handler_management_post_extensions ( if (GNUNET_OK != verify_extensions_from_json (extensions, &sec)) { - GNUNET_JSON_parse_free (top_spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, @@ -306,7 +293,6 @@ CLEANUP: } } GNUNET_free (sec.extensions); - GNUNET_JSON_parse_free (top_spec); return ret; } diff --git a/src/exchange/taler-exchange-httpd_management_partners.c b/src/exchange/taler-exchange-httpd_management_partners.c index c63192c60..fc8a4207d 100644 --- a/src/exchange/taler-exchange-httpd_management_partners.c +++ b/src/exchange/taler-exchange-httpd_management_partners.c @@ -48,7 +48,7 @@ TEH_handler_management_partners ( &partner_pub), GNUNET_JSON_spec_fixed_auto ("master_sig", &master_sig), - GNUNET_JSON_spec_string ("partner_base_url", + TALER_JSON_spec_web_url ("partner_base_url", &partner_base_url), TALER_JSON_spec_amount ("wad_fee", TEH_currency, @@ -113,7 +113,7 @@ TEH_handler_management_partners ( } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { - /* FIXME: check for idempotency! */ + /* FIXME-#7271: check for idempotency! */ return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT, diff --git a/src/exchange/taler-exchange-httpd_management_post_keys.c b/src/exchange/taler-exchange-httpd_management_post_keys.c index df351ad5f..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 @@ -47,6 +47,16 @@ struct DenomSig */ 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; + }; @@ -128,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, @@ -151,66 +161,9 @@ add_keys (void *cls, "lookup denomination key"); return qs; } - if (0 == qs) - { - enum GNUNET_GenericReturnValue rv; - - rv = TEH_keys_load_fees (akc->ksh, - &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 */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - 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.fees, - &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)); @@ -220,10 +173,9 @@ add_keys (void *cls, 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) @@ -245,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 ( @@ -263,47 +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 */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - 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)); @@ -312,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) { @@ -334,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; { @@ -363,34 +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_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"); - } GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received /management/keys\n"); + "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_INTERNAL_SERVER_ERROR, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, "no key state (not even for management)"); } + 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]; @@ -407,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); @@ -447,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", @@ -482,9 +470,7 @@ TEH_handler_management_post_keys ( &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_wire_disable.c b/src/exchange/taler-exchange-httpd_management_wire_disable.c index 34825eda3..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 () diff --git a/src/exchange/taler-exchange-httpd_management_wire_enable.c b/src/exchange/taler-exchange-httpd_management_wire_enable.c index 25ee0eeac..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 () }; @@ -179,17 +233,23 @@ TEH_handler_management_post_wire ( 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, @@ -198,11 +258,16 @@ TEH_handler_management_post_wire ( } 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, @@ -218,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, @@ -237,6 +303,7 @@ TEH_handler_management_post_wire ( &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 dcfa87ef5..cb87592a5 100644 --- a/src/exchange/taler-exchange-httpd_management_wire_fees.c +++ b/src/exchange/taler-exchange-httpd_management_wire_fees.c @@ -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" /** diff --git a/src/exchange/taler-exchange-httpd_melt.c b/src/exchange/taler-exchange-httpd_melt.c index 1e5c92e18..ac3902e3f 100644 --- a/src/exchange/taler-exchange-httpd_melt.c +++ b/src/exchange/taler-exchange-httpd_melt.c @@ -58,6 +58,7 @@ reply_melt_success (struct MHD_Connection *connection, &pub, &sig))) { + GNUNET_break (0); return TALER_MHD_reply_with_ec (connection, ec, NULL); @@ -288,7 +289,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; @@ -338,12 +339,12 @@ check_melt_valid (struct MHD_Connection *connection, TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, NULL); } - switch (dk->denom_pub.cipher) + switch (dk->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; break; default: diff --git a/src/exchange/taler-exchange-httpd_metrics.h b/src/exchange/taler-exchange-httpd_metrics.h index 8f6804355..318113c1f 100644 --- a/src/exchange/taler-exchange-httpd_metrics.h +++ b/src/exchange/taler-exchange-httpd_metrics.h @@ -61,7 +61,8 @@ enum TEH_MetricTypeSuccess TEH_MT_SUCCESS_BATCH_WITHDRAW = 3, TEH_MT_SUCCESS_MELT = 4, TEH_MT_SUCCESS_REFRESH_REVEAL = 5, - TEH_MT_SUCCESS_COUNT = 6 /* MUST BE LAST! */ + TEH_MT_SUCCESS_AGE_WITHDRAW_REVEAL = 6, + TEH_MT_SUCCESS_COUNT = 7 /* MUST BE LAST! */ }; /** diff --git a/src/exchange/taler-exchange-httpd_purses_create.c b/src/exchange/taler-exchange-httpd_purses_create.c index 130f9faec..2de9468fe 100644 --- a/src/exchange/taler-exchange-httpd_purses_create.c +++ b/src/exchange/taler-exchange-httpd_purses_create.c @@ -430,7 +430,7 @@ TEH_handler_purses_create ( .pd.purse_pub = *purse_pub, .exchange_timestamp = GNUNET_TIME_timestamp_get () }; - json_t *deposits; + const json_t *deposits; json_t *deposit; unsigned int idx; struct GNUNET_JSON_Specification spec[] = { @@ -449,8 +449,8 @@ TEH_handler_purses_create ( &pcc.purse_sig), GNUNET_JSON_spec_fixed_auto ("h_contract_terms", &pcc.pd.h_contract_terms), - GNUNET_JSON_spec_json ("deposits", - &deposits), + GNUNET_JSON_spec_array_const ("deposits", + &deposits), GNUNET_JSON_spec_timestamp ("purse_expiration", &pcc.pd.purse_expiration), GNUNET_JSON_spec_end () @@ -482,7 +482,6 @@ TEH_handler_purses_create ( pcc.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_PURSE_CREATE_EXPIRATION_BEFORE_NOW, @@ -491,7 +490,6 @@ TEH_handler_purses_create ( if (GNUNET_TIME_absolute_is_never (pcc.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_PURSE_CREATE_EXPIRATION_IS_NEVER, @@ -502,7 +500,6 @@ TEH_handler_purses_create ( (pcc.num_coins > TALER_MAX_FRESH_COINS) ) { 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, @@ -515,7 +512,6 @@ TEH_handler_purses_create ( 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, @@ -547,7 +543,6 @@ TEH_handler_purses_create ( deposit); if (GNUNET_OK != res) { - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<idx; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -559,7 +554,6 @@ TEH_handler_purses_create ( &pcc.deposit_total)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); GNUNET_free (pcc.coins); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, @@ -579,7 +573,6 @@ TEH_handler_purses_create ( &pcc.purse_sig)) { TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -597,7 +590,6 @@ TEH_handler_purses_create ( &pcc.econtract.econtract_sig)) ) { TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -612,7 +604,6 @@ TEH_handler_purses_create ( TEH_plugin->preflight (TEH_plugin->cls)) { GNUNET_break (0); - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -634,7 +625,6 @@ TEH_handler_purses_create ( &create_transaction, &pcc)) { - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -653,7 +643,6 @@ TEH_handler_purses_create ( for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); - GNUNET_JSON_parse_free (spec); return res; } } diff --git a/src/exchange/taler-exchange-httpd_purses_delete.c b/src/exchange/taler-exchange-httpd_purses_delete.c index 58cc78250..5bf7c24c9 100644 --- a/src/exchange/taler-exchange-httpd_purses_delete.c +++ b/src/exchange/taler-exchange-httpd_purses_delete.c @@ -57,29 +57,9 @@ TEH_handler_purses_delete ( TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, args[0]); } - { - const char *sig; - - sig = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - "Taler-Purse-Signature"); - if ( (NULL == sig) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (sig, - strlen (sig), - &purse_sig, - sizeof (purse_sig))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - (NULL == sig) - ? TALER_EC_GENERIC_PARAMETER_MISSING - : TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Taler-Purse-Signature"); - } - } - + 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)) diff --git a/src/exchange/taler-exchange-httpd_purses_deposit.c b/src/exchange/taler-exchange-httpd_purses_deposit.c index 291807aaa..8e4d5e41a 100644 --- a/src/exchange/taler-exchange-httpd_purses_deposit.c +++ b/src/exchange/taler-exchange-httpd_purses_deposit.c @@ -329,12 +329,12 @@ TEH_handler_purses_deposit ( .purse_pub = purse_pub, .exchange_timestamp = GNUNET_TIME_timestamp_get () }; - json_t *deposits; + const json_t *deposits; json_t *deposit; unsigned int idx; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("deposits", - &deposits), + GNUNET_JSON_spec_array_const ("deposits", + &deposits), GNUNET_JSON_spec_end () }; @@ -363,7 +363,6 @@ TEH_handler_purses_deposit ( (pcc.num_coins > TALER_MAX_FRESH_COINS) ) { 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, @@ -375,6 +374,7 @@ TEH_handler_purses_deposit ( 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, @@ -385,7 +385,8 @@ TEH_handler_purses_deposit ( &pcc.deposit_total, &pcc.h_contract_terms, &merge_timestamp, - &was_deleted); + &was_deleted, + &was_refunded); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -408,7 +409,7 @@ TEH_handler_purses_deposit ( case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; /* handled below */ } - if (GNUNET_TIME_absolute_is_past (pcc.purse_expiration.abs_time) || + if (was_refunded || was_deleted) { return TALER_MHD_reply_with_error ( @@ -435,7 +436,6 @@ TEH_handler_purses_deposit ( deposit); if (GNUNET_OK != res) { - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<idx; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -447,7 +447,6 @@ TEH_handler_purses_deposit ( TEH_plugin->preflight (TEH_plugin->cls)) { GNUNET_break (0); - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -469,7 +468,6 @@ TEH_handler_purses_deposit ( &deposit_transaction, &pcc)) { - GNUNET_JSON_parse_free (spec); for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); @@ -501,7 +499,6 @@ TEH_handler_purses_deposit ( for (unsigned int i = 0; i<pcc.num_coins; i++) TEH_common_purse_deposit_free_coin (&pcc.coins[i]); GNUNET_free (pcc.coins); - GNUNET_JSON_parse_free (spec); return res; } } diff --git a/src/exchange/taler-exchange-httpd_purses_get.c b/src/exchange/taler-exchange-httpd_purses_get.c index 434798a80..22328fe09 100644 --- a/src/exchange/taler-exchange-httpd_purses_get.c +++ b/src/exchange/taler-exchange-httpd_purses_get.c @@ -57,6 +57,12 @@ struct GetContext 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; @@ -153,6 +159,12 @@ gc_cleanup (struct TEH_RequestContext *rc) 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); } @@ -208,6 +220,7 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, { struct GetContext *gc = rc->rh_ctx; bool purse_deleted; + bool purse_refunded; MHD_RESULT res; if (NULL == gc) @@ -243,36 +256,8 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, args[1]); } - { - 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; - struct GNUNET_TIME_Relative timeout; - - 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); - gc->timeout = GNUNET_TIME_relative_to_absolute (timeout); - } - } - + TALER_MHD_parse_request_timeout (rc->connection, + &gc->timeout); if ( (GNUNET_TIME_absolute_is_future (gc->timeout)) && (NULL == gc->eh) ) { @@ -299,6 +284,20 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, 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 */ @@ -314,7 +313,8 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, &gc->deposited, &gc->h_contract, &gc->merge_timestamp, - &purse_deleted); + &purse_deleted, + &purse_refunded); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -337,41 +337,12 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; /* handled below */ } - if (GNUNET_TIME_absolute_cmp (gc->timeout, - >, - gc->purse_expiration.abs_time)) - { - /* Timeout too high, need to replace event handler */ - 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 - }; - struct GNUNET_DB_EventHandler *eh2; - - gc->timeout = gc->purse_expiration.abs_time; - eh2 = TEH_plugin->event_listen ( - TEH_plugin->cls, - GNUNET_TIME_absolute_get_remaining (gc->timeout), - &rep.header, - &db_event_cb, - rc); - if (NULL == eh2) - { - GNUNET_break (0); - gc->timeout = GNUNET_TIME_UNIT_ZERO_ABS; - } - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - gc->eh); - gc->eh = eh2; - } } - if (GNUNET_TIME_absolute_is_past (gc->purse_expiration.abs_time) || + 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 @@ -413,7 +384,10 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, 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_, @@ -422,10 +396,16 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, &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, @@ -444,6 +424,7 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc, GNUNET_JSON_pack_timestamp ("deposit_timestamp", dt)) ); + } } return res; } diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c index d246263f1..fb5ce4d90 100644 --- a/src/exchange/taler-exchange-httpd_purses_merge.c +++ b/src/exchange/taler-exchange-httpd_purses_merge.c @@ -34,7 +34,6 @@ #include "taler-exchange-httpd_responses.h" #include "taler_exchangedb_lib.h" #include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_wire.h" /** @@ -309,6 +308,7 @@ merge_transaction (void *cls, TEH_plugin->cls, required, &pcc->h_payto, + &pcc->reserve_pub, &pcc->kyc.requirement_row); GNUNET_free (required); if (GNUNET_DB_STATUS_HARD_ERROR == qs) @@ -370,13 +370,15 @@ merge_transaction (void *cls, 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); + &reserve_pub, + &refunded); if (qs <= 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -390,18 +392,39 @@ merge_transaction (void *cls, "select purse merge"); return qs; } - *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)); + 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; } @@ -421,8 +444,8 @@ TEH_handler_purses_merge ( .exchange_timestamp = GNUNET_TIME_timestamp_get () }; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("payto_uri", - &pcc.payto_uri), + 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", @@ -625,53 +648,6 @@ TEH_handler_purses_merge ( } } - if (GNUNET_TIME_absolute_is_past (pcc.purse_expiration.abs_time)) - { - struct TALER_PurseMergeSignatureP merge_sig; - struct GNUNET_TIME_Timestamp merge_timestamp; - char *partner_url = NULL; - struct TALER_ReservePublicKeyP reserve_pub; - - qs = TEH_plugin->select_purse_merge (TEH_plugin->cls, - pcc.purse_pub, - &merge_sig, - &merge_timestamp, - &partner_url, - &reserve_pub); - if (qs <= 0) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_GONE, - TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, - NULL); - } - if (0 != - GNUNET_memcmp (&merge_sig, - &pcc.merge_sig)) - { - MHD_RESULT mhd_res; - - mhd_res = 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 mhd_res; - } - GNUNET_free (partner_url); - /* request was idempotent, return success! */ - return reply_merge_success (connection, - &pcc); - } - /* execute transaction */ { MHD_RESULT mhd_ret; diff --git a/src/exchange/taler-exchange-httpd_recoup-refresh.c b/src/exchange/taler-exchange-httpd_recoup-refresh.c index d52dabda0..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. @@ -175,8 +175,8 @@ verify_and_execute_recoup_refresh ( struct MHD_Connection *connection, const struct TALER_CoinPublicInfo *coin, const struct TALER_ExchangeWithdrawValues *exchange_vals, - const union TALER_DenominationBlindingKeyP *coin_bks, - const struct TALER_CsNonce *nonce, + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, + const union GNUNET_CRYPTO_BlindSessionNonce *nonce, const struct TALER_CoinSpendSignatureP *coin_sig) { struct RecoupContext pc; @@ -219,12 +219,12 @@ verify_and_execute_recoup_refresh ( } /* check denomination signature */ - switch (dk->denom_pub.cipher) + switch (dk->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; break; default: @@ -265,6 +265,7 @@ verify_and_execute_recoup_refresh ( if (GNUNET_OK != TALER_denom_blind (&dk->denom_pub, coin_bks, + nonce, &coin->h_age_commitment, &coin->coin_pub, exchange_vals, @@ -278,9 +279,6 @@ verify_and_execute_recoup_refresh ( TALER_EC_EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED, NULL); } - if (TALER_DENOMINATION_CS == blinded_planchet.cipher) - blinded_planchet.details.cs_blinded_planchet.nonce - = *nonce; TALER_coin_ev_hash (&blinded_planchet, &coin->denom_pub_hash, &h_blind); @@ -375,10 +373,11 @@ TEH_handler_recoup_refresh (struct MHD_Connection *connection, { enum GNUNET_GenericReturnValue ret; struct TALER_CoinPublicInfo coin = {0}; - union TALER_DenominationBlindingKeyP coin_bks; + union GNUNET_CRYPTO_BlindingSecretP coin_bks; struct TALER_CoinSpendSignatureP coin_sig; struct TALER_ExchangeWithdrawValues exchange_vals; - struct TALER_CsNonce nonce; + 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), @@ -394,19 +393,17 @@ TEH_handler_recoup_refresh (struct MHD_Connection *connection, 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), - NULL), + &no_nonce), GNUNET_JSON_spec_end () }; memset (&coin, 0, sizeof (coin)); - memset (&nonce, - 0, - sizeof (nonce)); coin.coin_pub = *coin_pub; ret = TALER_MHD_parse_json_data (connection, root, @@ -422,7 +419,9 @@ TEH_handler_recoup_refresh (struct MHD_Connection *connection, &coin, &exchange_vals, &coin_bks, - &nonce, + no_nonce + ? NULL + : &nonce, &coin_sig); GNUNET_JSON_parse_free (spec); return res; diff --git a/src/exchange/taler-exchange-httpd_recoup.c b/src/exchange/taler-exchange-httpd_recoup.c index 349c2b94a..afbbd7474 100644 --- a/src/exchange/taler-exchange-httpd_recoup.c +++ b/src/exchange/taler-exchange-httpd_recoup.c @@ -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. @@ -178,8 +178,8 @@ verify_and_execute_recoup ( struct MHD_Connection *connection, const struct TALER_CoinPublicInfo *coin, const struct TALER_ExchangeWithdrawValues *exchange_vals, - const union TALER_DenominationBlindingKeyP *coin_bks, - const struct TALER_CsNonce *nonce, + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, + const union GNUNET_CRYPTO_BlindSessionNonce *nonce, const struct TALER_CoinSpendSignatureP *coin_sig) { struct RecoupContext pc; @@ -221,12 +221,12 @@ verify_and_execute_recoup ( } /* check denomination signature */ - switch (dk->denom_pub.cipher) + switch (dk->denom_pub.bsign_pub_key->cipher) { - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; break; - case TALER_DENOMINATION_CS: + case GNUNET_CRYPTO_BSA_CS: TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; break; default: @@ -270,6 +270,7 @@ verify_and_execute_recoup ( if (GNUNET_OK != TALER_denom_blind (&dk->denom_pub, coin_bks, + nonce, &coin->h_age_commitment, &coin->coin_pub, exchange_vals, @@ -283,20 +284,9 @@ verify_and_execute_recoup ( TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, NULL); } - if (TALER_DENOMINATION_CS == blinded_planchet.cipher) - blinded_planchet.details.cs_blinded_planchet.nonce - = *nonce; - if (GNUNET_OK != - TALER_coin_ev_hash (&blinded_planchet, - &coin->denom_pub_hash, - &pc.h_coin_ev)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - } + TALER_coin_ev_hash (&blinded_planchet, + &coin->denom_pub_hash, + &pc.h_coin_ev); TALER_blinded_planchet_free (&blinded_planchet); } @@ -388,10 +378,11 @@ 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; - struct TALER_CsNonce nonce; + 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), @@ -407,19 +398,17 @@ TEH_handler_recoup (struct MHD_Connection *connection, 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), - NULL), + &no_nonce), GNUNET_JSON_spec_end () }; memset (&coin, 0, sizeof (coin)); - memset (&nonce, - 0, - sizeof (nonce)); coin.coin_pub = *coin_pub; ret = TALER_MHD_parse_json_data (connection, root, @@ -435,7 +424,9 @@ TEH_handler_recoup (struct MHD_Connection *connection, &coin, &exchange_vals, &coin_bks, - &nonce, + 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 08a85265c..5630051cf 100644 --- a/src/exchange/taler-exchange-httpd_refreshes_reveal.c +++ b/src/exchange/taler-exchange-httpd_refreshes_reveal.c @@ -111,9 +111,6 @@ struct RevealContext /** * Array of information about fresh coins being revealed. */ - /* FIXME: const would be nicer here, but we initialize - the 'alg_values' in the verification - routine; suboptimal to be fixed... */ struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrcs; /** @@ -159,14 +156,17 @@ check_commitment (struct RevealContext *rctx, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { - struct TALER_CsNonce nonces[rctx->num_fresh_coins]; - unsigned int aoff = 0; + 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->cipher != rctx->rcds[j].blinded_planchet.cipher) + if (dk->bsign_pub_key->cipher != + rctx->rcds[j].blinded_planchet.blinded_message->cipher) { GNUNET_break (0); *mhd_ret = TALER_MHD_reply_with_error ( @@ -176,9 +176,9 @@ check_commitment (struct RevealContext *rctx, NULL); return GNUNET_SYSERR; } - switch (dk->cipher) + switch (dk->bsign_pub_key->cipher) { - case TALER_DENOMINATION_INVALID: + case GNUNET_CRYPTO_BSA_INVALID: GNUNET_break (0); *mhd_ret = TALER_MHD_reply_with_error ( connection, @@ -186,44 +186,48 @@ check_commitment (struct RevealContext *rctx, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, NULL); return GNUNET_SYSERR; - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: continue; - case TALER_DENOMINATION_CS: - nonces[aoff] - = rctx->rcds[j].blinded_planchet.details.cs_blinded_planchet.nonce; - aoff++; + 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! - aoff = 0; 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; - alg_values->cipher = dk->cipher; - switch (dk->cipher) + 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 TALER_DENOMINATION_INVALID: + case GNUNET_CRYPTO_BSA_INVALID: GNUNET_assert (0); return GNUNET_SYSERR; - case TALER_DENOMINATION_RSA: + case GNUNET_CRYPTO_BSA_RSA: continue; - case TALER_DENOMINATION_CS: + 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[aoff] + .nonce = &nonces[j]->cs_nonce }; ec = TEH_keys_denomination_cs_r_pub ( &cdd, true, - &alg_values->details.cs_values); + &bi->details.cs_values); if (TALER_EC_NONE != ec) { *mhd_ret = TALER_MHD_reply_with_error (connection, @@ -232,7 +236,6 @@ check_commitment (struct RevealContext *rctx, NULL); return GNUNET_SYSERR; } - aoff++; } } } @@ -274,14 +277,11 @@ check_commitment (struct RevealContext *rctx, &ts); rce->new_coins = GNUNET_new_array (rctx->num_fresh_coins, struct TALER_RefreshCoinData); - aoff = 0; for (unsigned int j = 0; j<rctx->num_fresh_coins; j++) { - const struct TALER_DenominationPublicKey *dk - = &rctx->dks[j]->denom_pub; struct TALER_RefreshCoinData *rcd = &rce->new_coins[j]; struct TALER_CoinSpendPrivateKeyP coin_priv; - union TALER_DenominationBlindingKeyP bks; + union GNUNET_CRYPTO_BlindingSecretP bks; const struct TALER_ExchangeWithdrawValues *alg_value = &rctx->rrcs[j].exchange_vals; struct TALER_PlanchetDetail pd = {0}; @@ -314,8 +314,9 @@ check_commitment (struct RevealContext *rctx, &acp, &ts.key, &nacp)); - - TALER_age_commitment_hash (&nacp.commitment, &h); + TALER_age_commitment_hash (&nacp.commitment, + &h); + TALER_age_commitment_proof_free (&nacp); hac = &h; } @@ -323,16 +324,11 @@ check_commitment (struct RevealContext *rctx, TALER_planchet_prepare (rcd->dk, alg_value, &bks, + nonces[j], &coin_priv, hac, &c_hash, &pd)); - if (TALER_DENOMINATION_CS == dk->cipher) - { - pd.blinded_planchet.details.cs_blinded_planchet.nonce = - nonces[aoff]; - aoff++; - } rcd->blinded_planchet = pd.blinded_planchet; } } @@ -511,7 +507,7 @@ resolve_refreshes_reveal_denominations ( } } - old_dk = TEH_keys_denomination_by_hash2 ( + old_dk = TEH_keys_denomination_by_hash_from_state ( ksh, &rctx->melt.session.coin.denom_pub_hash, connection, @@ -536,13 +532,14 @@ resolve_refreshes_reveal_denominations ( -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 ( (TALER_DENOMINATION_CS == dks[i]->denom_pub.cipher) && + if ( (GNUNET_CRYPTO_BSA_CS == + dks[i]->denom_pub.bsign_pub_key->cipher) && (rctx->no_rms) ) { return TALER_MHD_reply_with_error ( @@ -724,7 +721,8 @@ clean_age: rcd->blinded_planchet = rrc->blinded_planchet; rcd->dk = &dks[i]->denom_pub; - if (rcd->blinded_planchet.cipher != rcd->dk->cipher) + if (rcd->blinded_planchet.blinded_message->cipher != + rcd->dk->bsign_pub_key->cipher) { GNUNET_break_op (0); ret = TALER_MHD_REPLY_JSON_PACK ( @@ -761,8 +759,8 @@ clean_age: csds[i].bp = &rcds[i].blinded_planchet; } ec = TEH_keys_denomination_batch_sign ( - csds, rctx->num_fresh_coins, + csds, true, bss); if (TALER_EC_NONE != ec) @@ -773,12 +771,17 @@ clean_age: 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"); + for (unsigned int r = 0; r<MAX_TRANSACTION_COMMIT_RETRIES; r++) { bool changed; @@ -795,12 +798,7 @@ clean_age: NULL); goto cleanup; } - for (unsigned int i = 0; i<rctx->num_fresh_coins; i++) - { - struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i]; - rrc->blinded_planchet = rcds[i].blinded_planchet; - } qs = TEH_plugin->insert_refresh_reveal ( TEH_plugin->cls, melt_serial_id, @@ -865,7 +863,10 @@ 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); TALER_blinded_planchet_free (&rrc->blinded_planchet); } @@ -983,26 +984,26 @@ 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; - json_t *old_age_commitment; + 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_json ("old_age_commitment", - &old_age_commitment), + GNUNET_JSON_spec_array_const ("old_age_commitment", + &old_age_commitment), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("rms", @@ -1053,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, @@ -1061,19 +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, - old_age_commitment, - 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 33ead7c69..b8bcf7c60 100644 --- a/src/exchange/taler-exchange-httpd_refund.c +++ b/src/exchange/taler-exchange-httpd_refund.c @@ -158,6 +158,7 @@ refund_transaction (void *cls, } if (conflict) { + GNUNET_break_op (0); *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, diff --git a/src/exchange/taler-exchange-httpd_reserves_attest.c b/src/exchange/taler-exchange-httpd_reserves_attest.c index 297d8ceec..7bbebaad7 100644 --- a/src/exchange/taler-exchange-httpd_reserves_attest.c +++ b/src/exchange/taler-exchange-httpd_reserves_attest.c @@ -68,7 +68,7 @@ struct ReserveAttestContext /** * List of requested details. */ - json_t *details; + const json_t *details; /** * Client signature approving the request. @@ -158,8 +158,6 @@ reply_reserve_attest_success (struct MHD_Connection *connection, * @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 birthdate birthdate of user, in format YYYY-MM-DD; can be NULL; - * digits can be 0 if exact day, month or year are unknown * @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 @@ -169,7 +167,6 @@ static void kyc_process_cb (void *cls, const struct TALER_PaytoHashP *h_payto, const char *provider_section, - const char *birthdate, struct GNUNET_TIME_Timestamp collection_time, struct GNUNET_TIME_Timestamp expiration_time, size_t enc_attributes_size, @@ -290,8 +287,8 @@ TEH_handler_reserves_attest (struct TEH_RequestContext *rc, struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_timestamp ("request_timestamp", &rsc.timestamp), - GNUNET_JSON_spec_json ("details", - &rsc.details), + GNUNET_JSON_spec_array_const ("details", + &rsc.details), GNUNET_JSON_spec_fixed_auto ("reserve_sig", &rsc.reserve_sig), GNUNET_JSON_spec_end () diff --git a/src/exchange/taler-exchange-httpd_reserves_close.c b/src/exchange/taler-exchange-httpd_reserves_close.c index 760f705c6..bbf234428 100644 --- a/src/exchange/taler-exchange-httpd_reserves_close.c +++ b/src/exchange/taler-exchange-httpd_reserves_close.c @@ -27,7 +27,7 @@ #include "taler_mhd_lib.h" #include "taler_json_lib.h" #include "taler_dbevents.h" -#include "taler-exchange-httpd_wire.h" +#include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_reserves_close.h" #include "taler-exchange-httpd_responses.h" @@ -272,6 +272,7 @@ reserve_close_transaction (void *cls, 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) @@ -366,8 +367,8 @@ TEH_handler_reserves_close (struct TEH_RequestContext *rc, GNUNET_JSON_spec_timestamp ("request_timestamp", &rcc.timestamp), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("payto_uri", - &rcc.payto_uri), + TALER_JSON_spec_payto_uri ("payto_uri", + &rcc.payto_uri), NULL), GNUNET_JSON_spec_fixed_auto ("reserve_sig", &rcc.reserve_sig), diff --git a/src/exchange/taler-exchange-httpd_reserves_get.c b/src/exchange/taler-exchange-httpd_reserves_get.c index 88f7aca9c..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-2022 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,191 +169,95 @@ db_event_cb (void *cls, } -/** - * Closure for #reserve_history_transaction. - */ -struct ReserveHistoryContext -{ - /** - * 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; - - /** - * Set to true if we did not find the reserve. - */ - bool not_found; -}; - - -/** - * 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 (!) - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -reserve_balance_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct ReserveHistoryContext *rsc = cls; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_reserve_balance (TEH_plugin->cls, - &rsc->reserve_pub, - &rsc->balance); - 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, - "get_reserve_balance"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - rsc->not_found = true; - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - rsc->not_found = false; - return qs; -} - - 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; - 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_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); + "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); } { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "get reserve balance", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &reserve_balance_transaction, - &rsc)) - { - if (NULL != eh) - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - eh); - return mhd_ret; - } - } - /* generate proper response */ - if (rsc.not_found) - { - 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_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); - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("balance", - &rsc.balance)); + 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 index b53a8641a..ae983682a 100644 --- a/src/exchange/taler-exchange-httpd_reserves_get_attest.c +++ b/src/exchange/taler-exchange-httpd_reserves_get_attest.c @@ -64,8 +64,6 @@ struct ReserveAttestContext * @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 birthdate birthdate of user, in format YYYY-MM-DD; can be NULL; - * digits can be 0 if exact day, month or year are unknown * @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 @@ -75,7 +73,6 @@ static void kyc_process_cb (void *cls, const struct TALER_PaytoHashP *h_payto, const char *provider_section, - const char *birthdate, struct GNUNET_TIME_Timestamp collection_time, struct GNUNET_TIME_Timestamp expiration_time, size_t enc_attributes_size, diff --git a/src/exchange/taler-exchange-httpd_reserves_history.c b/src/exchange/taler-exchange-httpd_reserves_history.c index ffdc6eaa4..056d4b0ef 100644 --- a/src/exchange/taler-exchange-httpd_reserves_history.c +++ b/src/exchange/taler-exchange-httpd_reserves_history.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2022 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 @@ -15,7 +15,7 @@ */ /** * @file taler-exchange-httpd_reserves_history.c - * @brief Handle /reserves/$RESERVE_PUB/history requests + * @brief Handle /reserves/$RESERVE_PUB HISTORY requests * @author Florian Dold * @author Benedikt Mueller * @author Christian Grothoff @@ -23,7 +23,6 @@ #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" @@ -32,263 +31,486 @@ /** - * How far do we allow a client's time to be off when - * checking the request timestamp? + * 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 */ -#define TIMESTAMP_TOLERANCE \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) +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; -/** - * Closure for #reserve_history_transaction. - */ -struct ReserveHistoryContext -{ - /** - * 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; - - /** - * History of the reserve, set in the callback. - */ - struct TALER_EXCHANGEDB_ReserveHistory *rh; - - /** - * Global fees applying to the request. - */ - const struct TEH_GlobalFee *gf; - - /** - * Current reserve balance. - */ - struct TALER_Amount balance; -}; + 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; -/** - * Send reserve history to client. - * - * @param connection connection to the client - * @param rhc reserve history to return - * @return MHD result code - */ -static MHD_RESULT -reply_reserve_history_success (struct MHD_Connection *connection, - const struct ReserveHistoryContext *rhc) -{ - const struct TALER_EXCHANGEDB_ReserveHistory *rh = rhc->rh; - json_t *json_history; + 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; - json_history = TEH_RESPONSE_compile_reserve_history (rh); - 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", - &rhc->balance), - GNUNET_JSON_pack_array_steal ("history", - json_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; } /** - * Function implementing /reserves/$RID/history transaction. 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. + * Add the headers we want to set for every /keys 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 + * @param cls the key state to use + * @param[in,out] response the response to modify */ -static enum GNUNET_DB_QueryStatus -reserve_history_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) +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 ReserveHistoryContext *rsc = cls; - enum GNUNET_DB_QueryStatus qs; + 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; - if (! TALER_amount_is_zero (&rsc->gf->fees.history)) + TALER_MHD_parse_request_number (rc->connection, + "start", + &start_off); { - bool balance_ok = false; - bool idempotent = true; - - qs = TEH_plugin->insert_history_request (TEH_plugin->cls, - rsc->reserve_pub, - &rsc->reserve_sig, - rsc->timestamp, - &rsc->gf->fees.history, - &balance_ok, - &idempotent); - 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, - "get_reserve_history"); - } - if (qs <= 0) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; - } - if (! balance_ok) + 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)) { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE, + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE, NULL); } - if (idempotent) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Idempotent /reserves/history request observed. Is caching working?\n"); - } } - qs = TEH_plugin->get_reserve_history (TEH_plugin->cls, - rsc->reserve_pub, - &rsc->balance, - &rsc->rh); - 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, - "get_reserve_history"); - } - return qs; -} - -MHD_RESULT -TEH_handler_reserves_history (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - struct ReserveHistoryContext rsc; - MHD_RESULT mhd_ret; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_timestamp ("request_timestamp", - &rsc.timestamp), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &rsc.reserve_sig), - GNUNET_JSON_spec_end () - }; - struct GNUNET_TIME_Timestamp now; - - rsc.reserve_pub = reserve_pub; + /* Get etag */ { - enum GNUNET_GenericReturnValue res; + const char *etags; - res = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_SYSERR == res) + etags = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != etags) { - GNUNET_break (0); - return MHD_NO; /* hard failure */ + 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; + } } - if (GNUNET_NO == res) + else { - GNUNET_break_op (0); - return MHD_YES; /* failure */ + etag_in = start_off; } } - 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); - } + { - struct TEH_KeyStateHandle *keys; + enum GNUNET_DB_QueryStatus qs; - keys = TEH_keys_get_state (); - if (NULL == keys) + 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); - GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + 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; } - rsc.gf = TEH_keys_global_fee_by_time (keys, - rsc.timestamp); } - if (NULL == rsc.gf) + + GNUNET_snprintf (etagp, + sizeof (etagp), + "\"%llu\"", + (unsigned long long) etag_out); + if (etag_in == etag_out) { - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, - NULL); + return TEH_RESPONSE_reply_not_modified (rc->connection, + etagp, + &add_response_headers, + NULL); } - if (GNUNET_OK != - TALER_wallet_reserve_history_verify (rsc.timestamp, - &rsc.gf->fees.history, - reserve_pub, - &rsc.reserve_sig)) + if (NULL == rh) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_HISTORY_BAD_SIGNATURE, - NULL); + /* 204: empty history */ + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + http_status = MHD_HTTP_NO_CONTENT; } - rsc.rh = NULL; - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "get reserve history", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &reserve_history_transaction, - &rsc)) + else { - return mhd_ret; + 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)); } - if (NULL == rsc.rh) + add_response_headers (NULL, + resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etagp)); { - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_RESERVES_STATUS_UNKNOWN, - NULL); + MHD_RESULT ret; + + ret = MHD_queue_response (rc->connection, + http_status, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; } - mhd_ret = reply_reserve_history_success (rc->connection, - &rsc); - TEH_plugin->free_reserve_history (TEH_plugin->cls, - rsc.rh); - return mhd_ret; } diff --git a/src/exchange/taler-exchange-httpd_reserves_history.h b/src/exchange/taler-exchange-httpd_reserves_history.h index e02cb4d9b..e1bd7ae1b 100644 --- a/src/exchange/taler-exchange-httpd_reserves_history.h +++ b/src/exchange/taler-exchange-httpd_reserves_history.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2022 Taler Systems SA + 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 @@ -17,26 +17,27 @@ * @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 POST "/reserves/$RID/history" request. + * Handle a GET "/reserves/$RID/history" 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_history (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); +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 index 6909c862a..5aadc9e40 100644 --- a/src/exchange/taler-exchange-httpd_reserves_open.c +++ b/src/exchange/taler-exchange-httpd_reserves_open.c @@ -188,6 +188,7 @@ reserve_open_transaction (void *cls, { struct ReserveOpenContext *rsc = cls; enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount reserve_balance; for (unsigned int i = 0; i<rsc->payments_len; i++) { @@ -258,6 +259,7 @@ reserve_open_transaction (void *cls, &rsc->gf->fees.account, /* outputs */ &rsc->no_funds, + &reserve_balance, &rsc->open_cost, &rsc->reserve_expiration); switch (qs) @@ -289,6 +291,7 @@ reserve_open_transaction (void *cls, = 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; @@ -303,7 +306,7 @@ TEH_handler_reserves_open (struct TEH_RequestContext *rc, const json_t *root) { struct ReserveOpenContext rsc; - json_t *payments; + const json_t *payments; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_timestamp ("request_timestamp", &rsc.timestamp), @@ -313,8 +316,8 @@ TEH_handler_reserves_open (struct TEH_RequestContext *rc, &rsc.reserve_sig), GNUNET_JSON_spec_uint32 ("purse_limit", &rsc.purse_limit), - GNUNET_JSON_spec_json ("payments", - &payments), + GNUNET_JSON_spec_array_const ("payments", + &payments), TALER_JSON_spec_amount ("reserve_payment", TEH_currency, &rsc.reserve_payment), @@ -403,7 +406,6 @@ TEH_handler_reserves_open (struct TEH_RequestContext *rc, if (NULL == keys) { GNUNET_break (0); - GNUNET_JSON_parse_free (spec); cleanup_rsc (&rsc); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, diff --git a/src/exchange/taler-exchange-httpd_reserves_purse.c b/src/exchange/taler-exchange-httpd_reserves_purse.c index 5e39f810f..5e06db206 100644 --- a/src/exchange/taler-exchange-httpd_reserves_purse.c +++ b/src/exchange/taler-exchange-httpd_reserves_purse.c @@ -218,6 +218,7 @@ purse_transaction (void *cls, TEH_plugin->cls, required, &rpc->h_payto, + rpc->reserve_pub, &rpc->kyc.requirement_row); GNUNET_free (required); if (GNUNET_DB_STATUS_HARD_ERROR == qs) @@ -362,6 +363,7 @@ purse_transaction (void *cls, 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 ( @@ -370,7 +372,8 @@ purse_transaction (void *cls, &merge_sig, &merge_timestamp, &partner_url, - &reserve_pub); + &reserve_pub, + &refunded); if (qs <= 0) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); @@ -383,6 +386,18 @@ purse_transaction (void *cls, "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, diff --git a/src/exchange/taler-exchange-httpd_reserves_status.c b/src/exchange/taler-exchange-httpd_reserves_status.c deleted file mode 100644 index 4e7b4f47c..000000000 --- a/src/exchange/taler-exchange-httpd_reserves_status.c +++ /dev/null @@ -1,243 +0,0 @@ -/* - 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_status.c - * @brief Handle /reserves/$RESERVE_PUB STATUS requests - * @author Florian Dold - * @author Benedikt Mueller - * @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_reserves_status.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_status_transaction. - */ -struct ReserveStatusContext -{ - /** - * Public key of the reserve the inquiry is about. - */ - const struct TALER_ReservePublicKeyP *reserve_pub; - - /** - * History of the reserve, set in the callback. - */ - struct TALER_EXCHANGEDB_ReserveHistory *rh; - - /** - * Sum of incoming transactions within the returned history. - * (currently not used). - */ - struct TALER_Amount balance_in; - - /** - * Sum of outgoing transactions within the returned history. - * (currently not used). - */ - struct TALER_Amount balance_out; - - /** - * Current reserve balance. - */ - struct TALER_Amount balance; -}; - - -/** - * Send reserve status to client. - * - * @param connection connection to the client - * @param rhc reserve history to return - * @return MHD result code - */ -static MHD_RESULT -reply_reserve_status_success (struct MHD_Connection *connection, - const struct ReserveStatusContext *rhc) -{ - const struct TALER_EXCHANGEDB_ReserveHistory *rh = rhc->rh; - json_t *json_history; - - json_history = TEH_RESPONSE_compile_reserve_history (rh); - 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", - &rhc->balance), - GNUNET_JSON_pack_array_steal ("history", - json_history)); -} - - -/** - * Function implementing /reserves/ STATUS transaction. - * Execute a /reserves/ STATUS. 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 ReserveStatusContext *` - * @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_status_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct ReserveStatusContext *rsc = cls; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_reserve_status (TEH_plugin->cls, - rsc->reserve_pub, - &rsc->balance_in, - &rsc->balance_out, - &rsc->rh); - 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, - "get_reserve_status"); - } - qs = TEH_plugin->get_reserve_balance (TEH_plugin->cls, - rsc->reserve_pub, - &rsc->balance); - 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, - "get_reserve_balance"); - } - return qs; -} - - -MHD_RESULT -TEH_handler_reserves_status (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - struct ReserveStatusContext rsc; - MHD_RESULT mhd_ret; - struct GNUNET_TIME_Timestamp timestamp; - struct TALER_ReserveSignatureP reserve_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_timestamp ("request_timestamp", - ×tamp), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &reserve_sig), - GNUNET_JSON_spec_end () - }; - struct GNUNET_TIME_Timestamp now; - - 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 */ - } - } - now = GNUNET_TIME_timestamp_get (); - if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, - 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_status_verify (timestamp, - reserve_pub, - &reserve_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_STATUS_BAD_SIGNATURE, - NULL); - } - rsc.rh = NULL; - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "get reserve status", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &reserve_status_transaction, - &rsc)) - { - return mhd_ret; - } - if (NULL == rsc.rh) - { - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_RESERVES_STATUS_UNKNOWN, - NULL); - } - mhd_ret = reply_reserve_status_success (rc->connection, - &rsc); - TEH_plugin->free_reserve_history (TEH_plugin->cls, - rsc.rh); - return mhd_ret; -} - - -/* end of taler-exchange-httpd_reserves_status.c */ diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index 835a47713..8993ea50f 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.c @@ -23,494 +23,17 @@ * @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_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->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_RESPONSE_reply_unknown_denom_pub_hash ( struct MHD_Connection *connection, @@ -644,443 +167,131 @@ TEH_RESPONSE_reply_coin_insufficient_funds ( const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_CoinSpendPublicKeyP *coin_pub) { - struct TALER_EXCHANGEDB_TransactionList *tl; - enum GNUNET_DB_QueryStatus qs; - json_t *history; - - TEH_plugin->rollback (TEH_plugin->cls); - if (GNUNET_OK != - TEH_plugin->start_read_only (TEH_plugin->cls, - "get_coin_transactions")) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - coin_pub, - &tl); - TEH_plugin->rollback (TEH_plugin->cls); - if (0 > qs) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - - history = TEH_RESPONSE_compile_transaction_history (coin_pub, - tl); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - if (NULL == history) - { - 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"); - } 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), + // FIXME - #7267: to be kept only for some of the error types! GNUNET_JSON_pack_data_auto ("h_denom_pub", - h_denom_pub), - GNUNET_JSON_pack_array_steal ("history", - history)); + h_denom_pub)); } -json_t * -TEH_RESPONSE_compile_reserve_history ( - const struct TALER_EXCHANGEDB_ReserveHistory *rh) +MHD_RESULT +TEH_RESPONSE_reply_coin_conflicting_contract ( + struct MHD_Connection *connection, + enum TALER_ErrorCode ec, + const struct TALER_MerchantWireHashP *h_wire) { - 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; + 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)); +} - 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; +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) + ); - 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; +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; - case TALER_EXCHANGEDB_RO_CLOSE_REQUEST: - { - const struct TALER_EXCHANGEDB_CloseRequest *crq = - pos->details.close_request; + switch (status) + { - 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; - } + 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); } - return json_history; + 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) + ); } -/** - * 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 ec error code to return - * @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_reserve_insufficient_funds ( +MHD_RESULT +TEH_RESPONSE_reply_reserve_insufficient_balance ( struct MHD_Connection *connection, enum TALER_ErrorCode ec, - const struct TALER_Amount *ebalance, - const struct TALER_Amount *withdraw_amount, - const struct TALER_EXCHANGEDB_ReserveHistory *rh) + const struct TALER_Amount *reserve_balance, + const struct TALER_Amount *balance_required, + const struct TALER_ReservePublicKeyP *reserve_pub) { - json_t *json_history; - - json_history = TEH_RESPONSE_compile_reserve_history (rh); - if (NULL == json_history) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to compile reserve history\n"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS, - NULL); - } return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_CONFLICT, TALER_JSON_pack_ec (ec), TALER_JSON_pack_amount ("balance", - ebalance), + reserve_balance), TALER_JSON_pack_amount ("requested_amount", - withdraw_amount), - GNUNET_JSON_pack_array_steal ("history", - json_history)); + balance_required)); } MHD_RESULT -TEH_RESPONSE_reply_reserve_insufficient_balance ( +TEH_RESPONSE_reply_reserve_age_restriction_required ( struct MHD_Connection *connection, - enum TALER_ErrorCode ec, - const struct TALER_Amount *balance_required, - const struct TALER_ReservePublicKeyP *reserve_pub) + uint16_t maximum_allowed_age) { - struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; - struct TALER_Amount balance; - enum GNUNET_DB_QueryStatus qs; - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_plugin->start_read_only (TEH_plugin->cls, - "get_reserve_history on insufficient balance")) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - /* The reserve does not have the required amount (actual - * amount + withdraw fee) */ - qs = TEH_plugin->get_reserve_history (TEH_plugin->cls, - reserve_pub, - &balance, - &rh); - TEH_plugin->rollback (TEH_plugin->cls); - if ( (qs < 0) || - (NULL == rh) ) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "reserve history"); - } - mhd_ret = reply_reserve_insufficient_funds ( + return TALER_MHD_REPLY_JSON_PACK ( connection, - ec, - &balance, - balance_required, - rh); - TEH_plugin->free_reserve_history (TEH_plugin->cls, - rh); - return mhd_ret; + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec (TALER_EC_EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED), + GNUNET_JSON_pack_uint64 ("maximum_allowed_age", + maximum_allowed_age)); } @@ -1167,4 +378,32 @@ TEH_RESPONSE_reply_aml_blocked (struct MHD_Connection *connection, } +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; +} + + /* end of taler-exchange-httpd_responses.c */ diff --git a/src/exchange/taler-exchange-httpd_responses.h b/src/exchange/taler-exchange-httpd_responses.h index 0db6968f8..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-2022 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,24 +24,14 @@ */ #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> - - -/** - * 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 - */ -json_t * -TEH_RESPONSE_compile_reserve_history ( - const struct TALER_EXCHANGEDB_ReserveHistory *rh); +#include "taler_exchangedb_plugin.h" /** @@ -64,6 +54,7 @@ TEH_RESPONSE_reply_unknown_denom_pub_hash ( * * @param connection connection to the client * @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 @@ -72,9 +63,24 @@ 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 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 @@ -155,6 +161,68 @@ TEH_RESPONSE_reply_coin_insufficient_funds ( 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); + +/** + * 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 h_age_commitment hash of the age commitment as found in the database + * @return MHD result code + */ +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_AgeCommitmentHash *h_age_commitment); + +/** * Fundamental details about a purse. */ struct TEH_PurseDetails @@ -200,16 +268,32 @@ TEH_RESPONSE_reply_purse_created ( /** - * Compile the transaction history of a coin into a JSON object. + * Callback used to set headers in a response. * - * @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 cls closure + * @param[in,out] resp response to modify */ -json_t * -TEH_RESPONSE_compile_transaction_history ( - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_EXCHANGEDB_TransactionList *tl); +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 2a6dc8776..18d96f955 100644 --- a/src/exchange/taler-exchange-httpd_transfers_get.c +++ b/src/exchange/taler-exchange-httpd_transfers_get.c @@ -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; }; @@ -120,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", @@ -248,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,29 +298,96 @@ struct WtidTransactionContext * @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, - 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) +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' */ @@ -292,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; @@ -317,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; @@ -339,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, diff --git a/src/exchange/taler-exchange-httpd_wire.c b/src/exchange/taler-exchange-httpd_wire.c deleted file mode 100644 index 34010462d..000000000 --- a/src/exchange/taler-exchange-httpd_wire.c +++ /dev/null @@ -1,648 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2015-2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU 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> - -/** - * 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 -{ - /** - * Cached reply for /wire response. - */ - struct MHD_Response *wire_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; - - /** - * HTTP status to return with this response. - */ - unsigned int http_status; - -}; - - -/** - * 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; - - -/** - * 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); - } - MHD_destroy_response (wsh->wire_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++; -} - - -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 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; - } -} - - -/** - * 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; - - /** - * Context we hash "everything" we add into. This is used - * to compute the etag. Technically, we only hash the - * master_sigs, as they imply the rest. - */ - struct GNUNET_HashContext *hc; - - /** - * 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; - - GNUNET_CRYPTO_hash_context_read (ac->hc, - master_sig, - sizeof (*master_sig)); - 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; - struct GNUNET_HashContext *hc; - - 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 - = TALER_MHD_make_error (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 - = TALER_MHD_make_error (TALER_EC_EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED, - NULL); - return wsh; - } - wire_fee_object = json_object (); - GNUNET_assert (NULL != wire_fee_object); - wsh->cache_expiration = GNUNET_TIME_UNIT_FOREVER_ABS; - hc = GNUNET_CRYPTO_hash_context_start (); - { - 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 - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED, - payto_uri); - json_decref (wire_accounts_array); - json_decref (wire_fee_object); - GNUNET_CRYPTO_hash_context_abort (hc); - return wsh; - } - if (NULL == json_object_get (wire_fee_object, - wire_method)) - { - struct AddContext ac = { - .wire_method = wire_method, - .wsh = wsh, - .a = json_array (), - .hc = hc - }; - - 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->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - wsh->wire_reply - = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_wire_fees"); - GNUNET_CRYPTO_hash_context_abort (hc); - return wsh; - } - if (0 == json_array_size (ac.a)) - { - json_decref (ac.a); - json_decref (wire_accounts_array); - json_decref (wire_fee_object); - wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - wsh->wire_reply - = TALER_MHD_make_error (TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, - wire_method); - GNUNET_free (wire_method); - GNUNET_CRYPTO_hash_context_abort (hc); - return wsh; - } - 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)); - } - GNUNET_free (wire_method); - } - } - - - wsh->wire_reply = TALER_MHD_MAKE_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)); - { - struct GNUNET_TIME_Timestamp m; - - m = GNUNET_TIME_absolute_to_timestamp (wsh->cache_expiration); - TALER_MHD_get_date_string (m.abs_time, - wsh->dat); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Setting 'Expires' header for '/wire' to '%s'\n", - wsh->dat); - GNUNET_break (MHD_YES == - MHD_add_response_header (wsh->wire_reply, - MHD_HTTP_HEADER_EXPIRES, - wsh->dat)); - } - /* Set cache control headers: our response varies depending on these headers */ - GNUNET_break (MHD_YES == - MHD_add_response_header (wsh->wire_reply, - MHD_HTTP_HEADER_VARY, - MHD_HTTP_HEADER_ACCEPT_ENCODING)); - /* Information is always public, revalidate after 1 day */ - GNUNET_break (MHD_YES == - MHD_add_response_header (wsh->wire_reply, - MHD_HTTP_HEADER_CACHE_CONTROL, - "public,max-age=86400")); - - { - struct GNUNET_HashCode h; - char etag[sizeof (h) * 2]; - char *end; - - GNUNET_CRYPTO_hash_context_finish (hc, - &h); - end = GNUNET_STRINGS_data_to_string (&h, - sizeof (h), - etag, - sizeof (etag)); - *end = '\0'; - wsh->etag = GNUNET_strdup (etag); - GNUNET_break (MHD_YES == - MHD_add_response_header (wsh->wire_reply, - MHD_HTTP_HEADER_ETAG, - etag)); - } - 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); - { - const char *etag; - - etag = MHD_lookup_connection_value (rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if ( (NULL != etag) && - (MHD_HTTP_OK == wsh->http_status) && - (NULL != wsh->etag) && - (0 == strcmp (etag, - wsh->etag)) ) - { - MHD_RESULT ret; - struct MHD_Response *resp; - - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_EXPIRES, - wsh->dat)); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - wsh->etag)); - ret = MHD_queue_response (rc->connection, - MHD_HTTP_NOT_MODIFIED, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } - } - return MHD_queue_response (rc->connection, - wsh->http_status, - wsh->wire_reply); -} - - -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; -} - - -/* 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 75595fe69..000000000 --- a/src/exchange/taler-exchange-httpd_wire.h +++ /dev/null @@ -1,84 +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); - - -/** - * 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_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 28addba43..000000000 --- a/src/exchange/taler-exchange-httpd_withdraw.c +++ /dev/null @@ -1,676 +0,0 @@ -/* - 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_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_kyclogic_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" - - -/** - * Context for #withdraw_transaction. - */ -struct WithdrawContext -{ - - /** - * Hash of the (blinded) message to be signed by the Exchange. - */ - struct TALER_BlindedCoinHashP h_coin_envelope; - - /** - * 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; - - /** - * KYC status for the operation. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - - /** - * Hash of the payto-URI representing the account - * from which the money was put into the reserve. - */ - struct TALER_PaytoHashP h_account_payto; - - /** - * Current time for the DB transaction. - */ - struct GNUNET_TIME_Timestamp now; - - /** - * 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 -withdraw_amount_cb (void *cls, - struct GNUNET_TIME_Absolute limit, - TALER_EXCHANGEDB_KycAmountCallback cb, - void *cb_cls) -{ - struct WithdrawContext *wc = cls; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Signaling amount %s for KYC check\n", - TALER_amount2s (&wc->collectable.amount_with_fee)); - if (GNUNET_OK != - cb (cb_cls, - &wc->collectable.amount_with_fee, - wc->now.abs_time)) - return; - qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( - TEH_plugin->cls, - &wc->h_account_payto, - limit, - cb, - cb_cls); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got %d additional transactions for this withdrawal and limit %llu\n", - qs, - (unsigned long long) limit.abs_value_us); - 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; -} - - -/** - * 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; - bool nonce_ok = false; - uint64_t ruuid; - const struct TALER_CsNonce *nonce; - const struct TALER_BlindedPlanchet *bp; - 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->collectable.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->collectable.reserve_pub, - &wc->h_account_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_WITHDRAW, - &wc->h_account_payto, - TEH_plugin->select_satisfied_kyc_processes, - TEH_plugin->cls, - &withdraw_amount_cb, - wc, - &kyc_required); - if (qs < 0) - { - 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, - "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_account_payto, - &wc->kyc.requirement_row); - GNUNET_free (kyc_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; - } - } - wc->kyc.ok = true; - bp = &wc->blinded_planchet; - nonce = (TALER_DENOMINATION_CS == bp->cipher) - ? &bp->details.cs_blinded_planchet.nonce - : NULL; - qs = TEH_plugin->do_withdraw (TEH_plugin->cls, - nonce, - &wc->collectable, - wc->now, - &found, - &balance_ok, - &nonce_ok, - &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, - "do_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 (! balance_ok) - { - TEH_plugin->rollback (TEH_plugin->cls); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Balance insufficient for /withdraw\n"); - *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( - connection, - TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, - &wc->collectable.amount_with_fee, - &wc->collectable.reserve_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! nonce_ok) - { - TEH_plugin->rollback (TEH_plugin->cls); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++; - 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->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 */ - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW]++; - *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 struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - struct WithdrawContext wc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &wc.collectable.reserve_sig), - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &wc.collectable.denom_pub_hash), - TALER_JSON_spec_blinded_planchet ("coin_ev", - &wc.blinded_planchet), - GNUNET_JSON_spec_end () - }; - enum TALER_ErrorCode ec; - struct TEH_DenominationKey *dk; - - memset (&wc, - 0, - sizeof (wc)); - wc.collectable.reserve_pub = *reserve_pub; - { - 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 (dk->denom_pub.cipher != wc.blinded_planchet.cipher) - { - /* denomination cipher and blinded planchet cipher not the same */ - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL); - } - } - - if (0 > - TALER_amount_add (&wc.collectable.amount_with_fee, - &dk->meta.value, - &dk->meta.fees.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); - } - - if (GNUNET_OK != - TALER_coin_ev_hash (&wc.blinded_planchet, - &wc.collectable.denom_pub_hash, - &wc.collectable.h_coin_envelope)) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - } - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_withdraw_verify (&wc.collectable.denom_pub_hash, - &wc.collectable.amount_with_fee, - &wc.collectable.h_coin_envelope, - &wc.collectable.reserve_pub, - &wc.collectable.reserve_sig)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, - NULL); - } - - { - struct TEH_CoinSignData csd = { - .h_denom_pub = &wc.collectable.denom_pub_hash, - .bp = &wc.blinded_planchet - }; - - /* Sign before transaction! */ - ec = TEH_keys_denomination_sign ( - &csd, - false, - &wc.collectable.sig); - } - if (TALER_EC_NONE != ec) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to sign coin: %d\n", - ec); - 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_REQUEST_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); - - if (! wc.kyc.ok) - return TEH_RESPONSE_reply_kyc_required (rc->connection, - &wc.h_account_payto, - &wc.kyc); - - if (TALER_AML_NORMAL != wc.aml_decision) - return TEH_RESPONSE_reply_aml_blocked (rc->connection, - wc.aml_decision); - - { - 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-httpd_withdraw.h b/src/exchange/taler-exchange-httpd_withdraw.h deleted file mode 100644 index 2ec76bb92..000000000 --- a/src/exchange/taler-exchange-httpd_withdraw.h +++ /dev/null @@ -1,47 +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.h - * @brief Handle /reserve/withdraw requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_WITHDRAW_H -#define TALER_EXCHANGE_HTTPD_WITHDRAW_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/reserves/$RESERVE_PUB/withdraw" request. Parses 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. - * - * @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_withdraw (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); - -#endif 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-transfer.c b/src/exchange/taler-exchange-transfer.c index 5a4aace9c..9724b41fc 100644 --- a/src/exchange/taler-exchange-transfer.c +++ b/src/exchange/taler-exchange-transfer.c @@ -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; } diff --git a/src/exchange/taler-exchange-wirewatch.c b/src/exchange/taler-exchange-wirewatch.c index 168e7b9b7..da5d9c098 100644 --- a/src/exchange/taler-exchange-wirewatch.c +++ b/src/exchange/taler-exchange-wirewatch.c @@ -58,6 +58,12 @@ static struct TALER_BANK_CreditHistoryHandle *hh; static bool hh_returned_data; /** + * Set to true if the request for history did not + * succeed because the account was unknown. + */ +static bool hh_account_404; + +/** * When did we start the last @e hh request? */ static struct GNUNET_TIME_Absolute hh_start_time; @@ -290,7 +296,7 @@ add_account_cb (void *cls, if (! in_ai->credit_enabled) return; /* not enabled for us, skip */ if ( (NULL != account_section) && - (0 != strcasecmp (ai->section_name, + (0 != strcasecmp (in_ai->section_name, account_section)) ) return; /* not enabled for us, skip */ if (NULL != ai) @@ -356,7 +362,6 @@ exchange_serve_process_config (void) GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No accounts enabled for credit!\n"); GNUNET_SCHEDULER_shutdown (); - global_ret = EXIT_INVALIDARGUMENT; return GNUNET_SYSERR; } return GNUNET_OK; @@ -472,9 +477,9 @@ transaction_completed (void) GNUNET_SCHEDULER_shutdown (); return; } - if (! hh_returned_data) + if (! (hh_returned_data || hh_account_404) ) { - /* Enforce long polling delay even if the server ignored it + /* Enforce long-polling delay even if the server ignored it and returned earlier */ struct GNUNET_TIME_Relative latency; struct GNUNET_TIME_Relative left; @@ -482,8 +487,17 @@ transaction_completed (void) 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); @@ -495,13 +509,11 @@ transaction_completed (void) * We got incoming transaction details from the bank. Add them * to the database. * - * @param batch_size desired batch size * @param details array of transaction details * @param details_length length of the @a details array */ static void -process_reply (unsigned int batch_size, - const struct TALER_BANK_CreditDetails *details, +process_reply (const struct TALER_BANK_CreditDetails *details, unsigned int details_length) { enum GNUNET_DB_QueryStatus qs; @@ -570,7 +582,6 @@ process_reply (unsigned int batch_size, qs = db_plugin->reserves_in_insert (db_plugin->cls, reserves, details_length, - batch_size, qss); switch (qs) { @@ -580,8 +591,8 @@ process_reply (unsigned int batch_size, return; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got DB soft error for batch2_reserves_in_insert (%u). Rolling back.\n", - batch_size); + "Got DB soft error for reserves_in_insert (%u). Rolling back.\n", + details_length); handle_soft_error (); return; default: @@ -618,7 +629,7 @@ process_reply (unsigned int batch_size, break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Imported transaction %llu.", + "Imported transaction %llu.\n", (unsigned long long) cd->serial_id); /* normal case */ progress = true; @@ -686,43 +697,23 @@ static void history_cb (void *cls, const struct TALER_BANK_CreditHistoryResponse *reply) { - static int batch_mode = -2; - (void) cls; - if (-2 == batch_mode) - { - const char *mode = getenv ("TALER_WIREWATCH_BATCH_SIZE"); - char dummy; - - if ( (NULL == mode) || - (1 != sscanf (mode, - "%d%c", - &batch_mode, - &dummy)) ) - { - if (NULL != mode) - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Bad batch mode `%s' specified\n", - mode); - batch_mode = 8; /* maximum supported is currently 8 */ - } - } GNUNET_assert (NULL == task); hh = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "History request returned with HTTP status %u\n", reply->http_status); switch (reply->http_status) { case MHD_HTTP_OK: - process_reply (batch_mode, - reply->details.success.details, - reply->details.success.details_length); + 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) { transaction_completed (); @@ -761,6 +752,7 @@ continue_with_shard (void *cls) (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, @@ -857,6 +849,17 @@ lock_shard (void *cls) 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); @@ -869,7 +872,7 @@ lock_shard (void *cls) job_name, GNUNET_STRINGS_relative_time_to_string ( wirewatch_idle_sleep_interval, - GNUNET_YES)); + true)); delayed_until = GNUNET_TIME_relative_to_absolute ( wirewatch_idle_sleep_interval); shard_open = false; @@ -947,6 +950,7 @@ run (void *cls, { GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_NO_RESTART; return; } rc = GNUNET_CURL_gnunet_rc_create (ctx); @@ -978,7 +982,7 @@ main (int argc, GNUNET_GETOPT_option_relative_time ('f', "longpoll-timeout", "DELAY", - "what is the timeout when asking the bank about new transactions", + "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", diff --git a/src/exchange/test_taler_exchange_httpd.conf b/src/exchange/test_taler_exchange_httpd.conf index 0af23b9df..7e7ff8b45 100644 --- a/src/exchange/test_taler_exchange_httpd.conf +++ b/src/exchange/test_taler_exchange_httpd.conf @@ -7,13 +7,14 @@ TALER_RUNTIME_DIR = ${TMPDIR:-${TMP:-/tmp}}/${USER:-}/taler-system-runtime/ # Currency supported by the exchange (can only be one) CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 -AML_THRESHOLD = EUR:1000000 [auditor] TINY_AMOUNT = EUR:0.01 [exchange] +AML_THRESHOLD = EUR:1000000 + # Directory with our terms of service. TERMS_DIR = ../../contrib/tos @@ -68,7 +69,7 @@ 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_rsa] |