From 5844a20f15cf6d35503386a717e9d582189a7261 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Wed, 24 Jul 2019 00:13:53 +0200 Subject: implement zombie check --- src/exchange/taler-exchange-httpd_deposit.c | 4 +- src/exchange/taler-exchange-httpd_payback.c | 2 +- src/exchange/taler-exchange-httpd_refresh_melt.c | 107 +++++++++++++++-------- src/exchange/taler-exchange-httpd_refund.c | 2 +- src/exchange/taler-exchange-httpd_responses.c | 2 +- src/exchangedb/plugin_exchangedb_postgres.c | 35 ++++---- src/include/taler_error_codes.h | 8 +- src/lib/test_exchange_api_revocation.c | 31 ++++--- src/lib/testing_api_cmd_payback.c | 101 ++++++++++++++------- 9 files changed, 188 insertions(+), 104 deletions(-) diff --git a/src/exchange/taler-exchange-httpd_deposit.c b/src/exchange/taler-exchange-httpd_deposit.c index 5320c9c75..51adacb7c 100644 --- a/src/exchange/taler-exchange-httpd_deposit.c +++ b/src/exchange/taler-exchange-httpd_deposit.c @@ -179,7 +179,9 @@ deposit_transaction (void *cls, /* Start with fee for THIS transaction */ spent = deposit->amount_with_fee; - /* add cost of all previous transactions */ + /* add cost of all previous transactions; skip PAYBACK as revoked + denominations are not eligible for deposit, and if we are the old coin + pub of a revoked coin (aka a zombie), then ONLY refresh is allowed. */ qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, session, &deposit->coin.coin_pub, diff --git a/src/exchange/taler-exchange-httpd_payback.c b/src/exchange/taler-exchange-httpd_payback.c index 195e5613c..8cfc1aecc 100644 --- a/src/exchange/taler-exchange-httpd_payback.c +++ b/src/exchange/taler-exchange-httpd_payback.c @@ -289,7 +289,7 @@ payback_transaction (void *cls, return GNUNET_DB_STATUS_HARD_ERROR; } - /* Calculate remaining balance. */ + /* Calculate remaining balance, including paybacks already applied. */ qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, session, &pc->coin->coin_pub, diff --git a/src/exchange/taler-exchange-httpd_refresh_melt.c b/src/exchange/taler-exchange-httpd_refresh_melt.c index 5674a12f8..8677d6270 100644 --- a/src/exchange/taler-exchange-httpd_refresh_melt.c +++ b/src/exchange/taler-exchange-httpd_refresh_melt.c @@ -48,11 +48,11 @@ */ static int reply_refresh_melt_insufficient_funds (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - struct TALER_Amount coin_value, - struct TALER_EXCHANGEDB_TransactionList *tl, - const struct TALER_Amount *requested, - const struct TALER_Amount *residual) + const struct TALER_CoinSpendPublicKeyP *coin_pub, + struct TALER_Amount coin_value, + struct TALER_EXCHANGEDB_TransactionList *tl, + const struct TALER_Amount *requested, + const struct TALER_Amount *residual) { json_t *history; @@ -64,9 +64,9 @@ reply_refresh_melt_insufficient_funds (struct MHD_Connection *connection, MHD_HTTP_FORBIDDEN, "{s:s, s:I, s:o, s:o, s:o, s:o, s:o}", "error", - "insufficient funds", - "code", - (json_int_t) TALER_EC_REFRESH_MELT_INSUFFICIENT_FUNDS, + "insufficient funds", + "code", + (json_int_t) TALER_EC_REFRESH_MELT_INSUFFICIENT_FUNDS, "coin_pub", GNUNET_JSON_from_data_auto (coin_pub), "original_value", @@ -90,8 +90,8 @@ reply_refresh_melt_insufficient_funds (struct MHD_Connection *connection, */ static int reply_refresh_melt_success (struct MHD_Connection *connection, - const struct TALER_RefreshCommitmentP *rc, - uint32_t noreveal_index) + const struct TALER_RefreshCommitmentP *rc, + uint32_t noreveal_index) { struct TALER_RefreshMeltConfirmationPS body; struct TALER_ExchangePublicKeyP pub; @@ -139,6 +139,13 @@ struct RefreshMeltContext */ struct TALER_EXCHANGEDB_DenominationKeyIssueInformation *dki; + /** + * Set to #GNUNET_YES if this @a dki was revoked and the operation + * is thus only allowed for zombie coins where the transaction + * history includes a #TALER_EXCHANGEDB_TT_OLD_COIN_PAYBACK. + */ + int zombie_required; + }; @@ -168,11 +175,12 @@ refresh_check_melt (struct MHD_Connection *connection, /* Start with cost of this melt transaction */ spent = rmc->refresh_session.amount_with_fee; - /* add historic transaction costs of this coin */ + /* add historic transaction costs of this coin, including paybacks as + we might be a zombie coin */ qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, session, &rmc->refresh_session.coin.coin_pub, - GNUNET_NO, + GNUNET_YES, &tl); if (0 > qs) { @@ -181,16 +189,40 @@ refresh_check_melt (struct MHD_Connection *connection, TALER_EC_REFRESH_MELT_DB_FETCH_ERROR); return qs; } + if (rmc->zombie_required) + { + for (struct TALER_EXCHANGEDB_TransactionList *tp = tl; + NULL != tp; + tp = tp->next) + { + if (TALER_EXCHANGEDB_TT_OLD_COIN_PAYBACK == tp->type) + { + rmc->zombie_required = GNUNET_NO; /* was satisfied! */ + break; + } + } + if (rmc->zombie_required) + { + /* zombie status not satisfied */ + GNUNET_break (0); + TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, + tl); + *mhd_ret = TEH_RESPONSE_reply_external_error (connection, + TALER_EC_REFRESH_MELT_COIN_EXPIRED_NO_ZOMBIE, + "denomination expired"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } if (GNUNET_OK != TEH_DB_calculate_transaction_list_totals (tl, - &spent, - &spent)) + &spent, + &spent)) { GNUNET_break (0); TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, tl); *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, - TALER_EC_REFRESH_MELT_COIN_HISTORY_COMPUTATION_FAILED); + TALER_EC_REFRESH_MELT_COIN_HISTORY_COMPUTATION_FAILED); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -206,11 +238,11 @@ refresh_check_melt (struct MHD_Connection *connection, &spent, &rmc->refresh_session.amount_with_fee)); *mhd_ret = reply_refresh_melt_insufficient_funds (connection, - &rmc->refresh_session.coin.coin_pub, - coin_value, - tl, - &rmc->refresh_session.amount_with_fee, - &coin_residual); + &rmc->refresh_session.coin.coin_pub, + coin_value, + tl, + &rmc->refresh_session.amount_with_fee, + &coin_residual); TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, tl); return GNUNET_DB_STATUS_HARD_ERROR; @@ -245,9 +277,9 @@ refresh_check_melt (struct MHD_Connection *connection, */ static enum GNUNET_DB_QueryStatus refresh_melt_transaction (void *cls, - struct MHD_Connection *connection, - struct TALER_EXCHANGEDB_Session *session, - int *mhd_ret) + struct MHD_Connection *connection, + struct TALER_EXCHANGEDB_Session *session, + int *mhd_ret) { struct RefreshMeltContext *rmc = cls; enum GNUNET_DB_QueryStatus qs; @@ -262,8 +294,8 @@ refresh_melt_transaction (void *cls, { TALER_LOG_DEBUG ("Found already-melted coin\n"); *mhd_ret = reply_refresh_melt_success (connection, - &rmc->refresh_session.rc, - noreveal_index); + &rmc->refresh_session.rc, + noreveal_index); /* Note: we return "hard error" to ensure the wrapper does not retry the transaction, and to also not generate a "fresh" response (as we would on "success") */ @@ -273,22 +305,22 @@ refresh_melt_transaction (void *cls, { if (GNUNET_DB_STATUS_HARD_ERROR == qs) *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, - TALER_EC_REFRESH_MELT_DB_FETCH_ERROR); + TALER_EC_REFRESH_MELT_DB_FETCH_ERROR); return qs; } /* check coin has enough funds remaining on it to cover melt cost */ qs = refresh_check_melt (connection, - session, - rmc, - mhd_ret); + session, + rmc, + mhd_ret); if (0 > qs) return qs; /* pick challenge and persist it */ rmc->refresh_session.noreveal_index = GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, - TALER_CNC_KAPPA); + TALER_CNC_KAPPA); if (0 >= (qs = TEH_plugin->insert_melt (TEH_plugin->cls, @@ -298,7 +330,7 @@ refresh_melt_transaction (void *cls, if (GNUNET_DB_STATUS_SOFT_ERROR != qs) { *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, - TALER_EC_REFRESH_MELT_DB_STORE_SESSION_ERROR); + TALER_EC_REFRESH_MELT_DB_STORE_SESSION_ERROR); return GNUNET_DB_STATUS_HARD_ERROR; } return qs; @@ -327,9 +359,9 @@ handle_refresh_melt (struct MHD_Connection *connection, struct TALER_Amount fee_refresh; TALER_amount_ntoh (&fee_refresh, - &rmc->dki->issue.properties.fee_refresh); + &rmc->dki->issue.properties.fee_refresh); if (TALER_amount_cmp (&fee_refresh, - &rmc->refresh_session.amount_with_fee) > 0) + &rmc->refresh_session.amount_with_fee) > 0) { GNUNET_break_op (0); return TEH_RESPONSE_reply_external_error (connection, @@ -358,8 +390,8 @@ handle_refresh_melt (struct MHD_Connection *connection, { GNUNET_break_op (0); return TEH_RESPONSE_reply_signature_invalid (connection, - TALER_EC_REFRESH_MELT_COIN_SIGNATURE_INVALID, - "confirm_sig"); + TALER_EC_REFRESH_MELT_COIN_SIGNATURE_INVALID, + "confirm_sig"); } } @@ -505,9 +537,8 @@ TEH_REFRESH_handler_refresh_melt (struct TEH_RequestHandler *rh, TEH_KS_DKU_ZOMBIE); if (NULL != dki) { - /* Test if zombie-condition is actually satisfied for the coin */ - if (0 /* FIXME: test if zombie-satisfied */) - rmc.dki = dki; + rmc.dki = dki; + rmc.zombie_required = GNUNET_YES; } } diff --git a/src/exchange/taler-exchange-httpd_refund.c b/src/exchange/taler-exchange-httpd_refund.c index 69b5cae96..5fea37da0 100644 --- a/src/exchange/taler-exchange-httpd_refund.c +++ b/src/exchange/taler-exchange-httpd_refund.c @@ -165,7 +165,7 @@ refund_transaction (void *cls, session, &refund->coin.coin_pub, GNUNET_NO, - &tl); + &tl); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index 3cf3e781b..f05e42604 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.c @@ -393,7 +393,7 @@ TEH_RESPONSE_reply_external_error (struct MHD_Connection *connection, */ int TEH_RESPONSE_reply_commit_error (struct MHD_Connection *connection, - enum TALER_ErrorCode ec) + enum TALER_ErrorCode ec) { return TEH_RESPONSE_reply_json_pack (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c index 56e9202b7..c95ed2616 100644 --- a/src/exchangedb/plugin_exchangedb_postgres.c +++ b/src/exchangedb/plugin_exchangedb_postgres.c @@ -322,11 +322,10 @@ postgres_create_tables (void *cls) ",h_coin_ev BYTEA NOT NULL CHECK(LENGTH(h_coin_ev)=64)" ",ev_sig BYTEA NOT NULL" ",PRIMARY KEY (rc, newcoin_index)" + ",UNIQUE (h_coin_ev)" ");"), GNUNET_PQ_make_try_execute ("CREATE INDEX refresh_revealed_coins_coin_pub_index ON " "refresh_revealed_coins (denom_pub_hash);"), - GNUNET_PQ_make_try_execute ("CREATE INDEX refresh_revealed_coins_h_coin_ev_index ON " - "refresh_revealed_coins (h_coin_ev);"), /* Table with the transfer keys of a refresh operation; includes the rc for which this is the link information, the @@ -1661,23 +1660,24 @@ postgres_prepare (PGconn *db_conn) affecting old coins of refreshed coins */ GNUNET_PQ_make_prepare ("payback_by_old_coin", "SELECT" - " pr.coin_pub" - ",pr.coin_sig" - ",pr.coin_blind" - ",pr.amount_val" - ",pr.amount_frac" - ",pr.amount_curr" - ",pr.timestamp" + " coin_pub" + ",coin_sig" + ",coin_blind" + ",amount_val" + ",amount_frac" + ",amount_curr" + ",timestamp" ",coins.denom_pub_hash" ",coins.denom_sig" - " FROM refresh_commitments" - " JOIN refresh_revealed_coins rrc" - " USING (rc)" - " JOIN payback_refresh pr" - " ON (rrc.coin_ev = pr.h_blind_ev)" + " FROM payback_refresh" " JOIN known_coins coins" - " ON (coins.coin_pub = pr.coin_pub)" - " WHERE old_coin_pub=$1" + " USING (coin_pub)" + " WHERE h_blind_ev IN" + " (SELECT rrc.h_coin_ev" + " FROM refresh_commitments" + " JOIN refresh_revealed_coins rrc" + " USING (rc)" + " WHERE old_coin_pub=$1)" " FOR UPDATE;", 1), /* Used in #postgres_get_reserve_history() */ @@ -4821,9 +4821,6 @@ postgres_get_coin_transactions (void *cls, /** #TALER_EXCHANGEDB_TT_REFUND */ { "get_refunds_by_coin", &add_coin_refund }, - /** #TALER_EXCHANGEDB_TT_OLD_COIN_PAYBACK */ - { "payback_by_old_coin", - &add_old_coin_payback }, { NULL, NULL } }; static const struct Work work_wp[] = { diff --git a/src/include/taler_error_codes.h b/src/include/taler_error_codes.h index 5767a73b2..cd99a40fc 100644 --- a/src/include/taler_error_codes.h +++ b/src/include/taler_error_codes.h @@ -523,6 +523,12 @@ enum TALER_ErrorCode */ TALER_EC_REFRESH_MELT_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1308, + /** + * The denomination of the given coin has past its expiration date and it is + * also not a valid zombie (that is, was not refreshed with the fresh coin + * being subjected to payback). + */ + TALER_EC_REFRESH_MELT_COIN_EXPIRED_NO_ZOMBIE = 1309, /** * The provided transfer keys do not match up with the @@ -624,7 +630,7 @@ enum TALER_ErrorCode */ TALER_EC_REFRESH_REVEAL_LINK_SIGNATURE_INVALID = 1362, - + /** * The coin specified in the link request is unknown to the exchange. * This response is provided with HTTP status code diff --git a/src/lib/test_exchange_api_revocation.c b/src/lib/test_exchange_api_revocation.c index 090b38b25..bd1d91c66 100644 --- a/src/lib/test_exchange_api_revocation.c +++ b/src/lib/test_exchange_api_revocation.c @@ -159,16 +159,27 @@ run (void *cls, "refresh-melt-1", CONFIG_FILE), /* Refund coin to original coin */ - TALER_TESTING_cmd_payback ("payback-1", + TALER_TESTING_cmd_payback ("payback-1a", MHD_HTTP_OK, - "refresh-reveal-1", - "EUR:5", + "refresh-reveal-1#0", + "EUR:1", "refresh-melt-1"), - /** - * Melt original coin AGAIN - * (EUR:4.00 = 3x EUR:1.03 + 7x EUR:0.13) */ + TALER_TESTING_cmd_payback ("payback-1b", + MHD_HTTP_OK, + "refresh-reveal-1#1", + "EUR:1", + "refresh-melt-1"), + TALER_TESTING_cmd_payback ("payback-1c", + MHD_HTTP_OK, + "refresh-reveal-1#2", + "EUR:1", + "refresh-melt-1"), + /* Melt original coin AGAIN (FIXME: this command + is simply WRONG as it neither matches + the EUR:3 that were paid back NOR is melt_double + precisely right here!) -- it always tries to MELT EUR:4, which is too much! */ TALER_TESTING_cmd_refresh_melt_double - ("refresh-melt-2", "EUR:4", + ("refresh-melt-2", "EUR:3", "withdraw-coin-1", MHD_HTTP_OK), /** * Complete (successful) melt operation, and withdraw the coins @@ -190,18 +201,18 @@ run (void *cls, TALER_TESTING_cmd_payback ("payback-2", MHD_HTTP_OK, "refresh-melt-2", - "EUR:5", + "EUR:1", "refresh-melt-2"), /* Refund original coin to reserve */ TALER_TESTING_cmd_payback ("payback-3", MHD_HTTP_OK, "withdraw-coin-1", - "EUR:5", + "EUR:1", NULL), /* Check the money is back with the reserve */ TALER_TESTING_cmd_status ("payback-reserve-status-1", "create-reserve-1", - "EUR:4.0", + "EUR:1.0", MHD_HTTP_OK), TALER_TESTING_cmd_end () }; diff --git a/src/lib/testing_api_cmd_payback.c b/src/lib/testing_api_cmd_payback.c index c7ea5091d..de394fe26 100644 --- a/src/lib/testing_api_cmd_payback.c +++ b/src/lib/testing_api_cmd_payback.c @@ -107,6 +107,51 @@ struct PaybackState }; + +/** + * Parser reference to a coin. + * + * @param coin_reference of format $LABEL['#' $INDEX]? + * @param cref[out] where we return a copy of $LABEL + * @param idx[out] where we set $INDEX + * @return #GNUNET_SYSERR if $INDEX is present but not numeric + */ +static int +parse_coin_reference (const char *coin_reference, + char **cref, + unsigned int *idx) +{ + const char *index; + + /* We allow command references of the form "$LABEL#$INDEX" or + just "$LABEL", which implies the index is 0. Figure out + which one it is. */ + index = strchr (coin_reference, '#'); + if (NULL == index) + { + *idx = 0; + *cref = GNUNET_strdup (coin_reference); + return GNUNET_OK; + } + *cref = GNUNET_strndup (coin_reference, + index - coin_reference); + if (1 != sscanf (index + 1, + "%u", + idx)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Numeric index (not `%s') required after `#' in command reference of command in %s:%u\n", + index, + __FILE__, + __LINE__); + GNUNET_free (*cref); + *cref = NULL; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + /** * Check the result of the payback request: checks whether * the HTTP response code is good, and that the coin that @@ -137,7 +182,6 @@ payback_cb (void *cls, struct TALER_TESTING_Command *cmd = &is->commands[is->ip]; const struct TALER_TESTING_Command *reserve_cmd; char *cref; - const char *index; unsigned int idx; ps->ph = NULL; @@ -155,34 +199,15 @@ payback_cb (void *cls, return; } - /* We allow command references of the form "$LABEL#$INDEX" or - just "$LABEL", which implies the index is 0. Figure out - which one it is. */ - index = strchr (ps->coin_reference, '#'); - if (NULL == index) + if (GNUNET_OK != + parse_coin_reference (ps->coin_reference, + &cref, + &idx)) { - idx = 0; - cref = GNUNET_strdup (ps->coin_reference); - } - else - { - cref = GNUNET_strndup (ps->coin_reference, - index - ps->coin_reference); - if (1 != sscanf (index, - "%u", - &idx)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Numeric index (not `%s') required after `#' in command reference of command %s in %s:%u\n", - index, - cmd->label, - __FILE__, - __LINE__); - TALER_TESTING_interpreter_fail (is); - GNUNET_free (cref); - return; - } + TALER_TESTING_interpreter_fail (is); + return; } + reserve_cmd = TALER_TESTING_interpreter_lookup_command (is, cref); GNUNET_free (cref); @@ -309,10 +334,22 @@ payback_run (void *cls, const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; const struct TALER_DenominationSignature *coin_sig; struct TALER_PlanchetSecretsP planchet; + char *cref; + unsigned int idx; ps->is = is; + if (GNUNET_OK != + parse_coin_reference (ps->coin_reference, + &cref, + &idx)) + { + TALER_TESTING_interpreter_fail (is); + return; + } + coin_cmd = TALER_TESTING_interpreter_lookup_command - (is, ps->coin_reference); + (is, cref); + GNUNET_free (cref); if (NULL == coin_cmd) { @@ -322,7 +359,7 @@ payback_run (void *cls, } if (GNUNET_OK != TALER_TESTING_get_trait_coin_priv - (coin_cmd, 0, &coin_priv)) + (coin_cmd, idx, &coin_priv)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); @@ -330,7 +367,7 @@ payback_run (void *cls, } if (GNUNET_OK != TALER_TESTING_get_trait_blinding_key - (coin_cmd, 0, &blinding_key)) + (coin_cmd, idx, &blinding_key)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); @@ -340,7 +377,7 @@ payback_run (void *cls, planchet.blinding_key = *blinding_key; if (GNUNET_OK != TALER_TESTING_get_trait_denom_pub - (coin_cmd, 0, &denom_pub)) + (coin_cmd, idx, &denom_pub)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); @@ -348,7 +385,7 @@ payback_run (void *cls, } if (GNUNET_OK != TALER_TESTING_get_trait_denom_sig - (coin_cmd, 0, &coin_sig)) + (coin_cmd, idx, &coin_sig)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); -- cgit v1.2.3