exchange/src/lib/exchange_api_recoup_refresh.c
Christian Grothoff e7aeec04f4
The current recoup API is broken. I guess this is another example where "trivial" API changes turn out to have (multiple!) unexpected consequences.
The current "/recoup" API does not have clear idempotency semantics, as we've discussed on the phone.  This is already bad by itself, as it makes it hard to write down what the API does other than "whatever the implementation does".

However, it actually breaks correctness in this (admittedly kinda contrived, but not impossible) case:

Say that we have a coin A obtained via withdrawal and a coin B obtained via refreshing coin A. Now the denominations of A gets revoked..

The wallet does a recoup of A for EUR:1.

Now the denomination of B also gets revoked.  The wallet recoups B (incidentally also for EUR:1) and now A can be recouped again for EUR:1.  But now the exchange is in a state where it will refuse a legitimate recoup request for A because the detection for an idempotent request kicks in.

This is IMHO bad API design, and the exchange should simply always recoup the maximum amount.

Furthermore, we usually follow the principle of "API calls that take up DB space are paid".  With the current recoup API, I can do many tiny recoup requests which the exchange then has to store, right?

I guess it would not be a big change to remove the "amount" value from the recoup/recoup-refresh request bodies, right?

- Florian
2022-01-11 12:47:35 +01:00

400 lines
12 KiB
C

/*
This file is part of TALER
Copyright (C) 2017-2021 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, see
<http://www.gnu.org/licenses/>
*/
/**
* @file lib/exchange_api_recoup_refresh.c
* @brief Implementation of the /recoup-refresh request of the exchange's HTTP API
* @author Christian Grothoff
*/
#include "platform.h"
#include <jansson.h>
#include <microhttpd.h> /* just for HTTP status codes */
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_json_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include "taler_json_lib.h"
#include "taler_exchange_service.h"
#include "exchange_api_handle.h"
#include "taler_signatures.h"
#include "exchange_api_curl_defaults.h"
/**
* @brief A Recoup Handle
*/
struct TALER_EXCHANGE_RecoupRefreshHandle
{
/**
* The connection to exchange this request handle will use
*/
struct TALER_EXCHANGE_Handle *exchange;
/**
* The url for this request.
*/
char *url;
/**
* Context for #TEH_curl_easy_post(). Keeps the data that must
* persist for Curl to make the upload.
*/
struct TALER_CURL_PostContext ctx;
/**
* Denomination key of the coin.
*/
struct TALER_EXCHANGE_DenomPublicKey pk;
/**
* Handle for the request.
*/
struct GNUNET_CURL_Job *job;
/**
* Function to call with the result.
*/
TALER_EXCHANGE_RecoupRefreshResultCallback cb;
/**
* Closure for @a cb.
*/
void *cb_cls;
/**
* Public key of the coin we are trying to get paid back.
*/
struct TALER_CoinSpendPublicKeyP coin_pub;
};
/**
* Parse a recoup-refresh response. If it is valid, call the callback.
*
* @param ph recoup handle
* @param json json reply with the signature
* @return #GNUNET_OK if the signature is valid and we called the callback;
* #GNUNET_SYSERR if not (callback must still be called)
*/
static enum GNUNET_GenericReturnValue
process_recoup_response (
const struct TALER_EXCHANGE_RecoupRefreshHandle *ph,
const json_t *json)
{
struct TALER_CoinSpendPublicKeyP old_coin_pub;
struct GNUNET_JSON_Specification spec_refresh[] = {
GNUNET_JSON_spec_fixed_auto ("old_coin_pub",
&old_coin_pub),
GNUNET_JSON_spec_end ()
};
struct TALER_EXCHANGE_HttpResponse hr = {
.reply = json,
.http_status = MHD_HTTP_OK
};
if (GNUNET_OK !=
GNUNET_JSON_parse (json,
spec_refresh,
NULL, NULL))
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
ph->cb (ph->cb_cls,
&hr,
&old_coin_pub);
return GNUNET_OK;
}
/**
* Function called when we're done processing the
* HTTP /recoup-refresh request.
*
* @param cls the `struct TALER_EXCHANGE_RecoupRefreshHandle`
* @param response_code HTTP response code, 0 on error
* @param response parsed JSON result, NULL on error
*/
static void
handle_recoup_refresh_finished (void *cls,
long response_code,
const void *response)
{
struct TALER_EXCHANGE_RecoupRefreshHandle *ph = cls;
const json_t *j = response;
struct TALER_EXCHANGE_HttpResponse hr = {
.reply = j,
.http_status = (unsigned int) response_code
};
ph->job = NULL;
switch (response_code)
{
case 0:
hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
break;
case MHD_HTTP_OK:
if (GNUNET_OK !=
process_recoup_response (ph,
j))
{
GNUNET_break_op (0);
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
hr.http_status = 0;
break;
}
TALER_EXCHANGE_recoup_refresh_cancel (ph);
return;
case MHD_HTTP_BAD_REQUEST:
/* This should never happen, either us or the exchange is buggy
(or API version conflict); just pass JSON reply to the application */
hr.ec = TALER_JSON_get_error_code (j);
hr.hint = TALER_JSON_get_error_hint (j);
break;
case MHD_HTTP_CONFLICT:
{
/* Insufficient funds, proof attached */
json_t *history;
struct TALER_Amount total;
struct TALER_DenominationHash h_denom_pub;
const struct TALER_EXCHANGE_DenomPublicKey *dki;
enum TALER_ErrorCode ec;
dki = &ph->pk;
history = json_object_get (j,
"history");
if (GNUNET_OK !=
TALER_EXCHANGE_verify_coin_history (dki,
dki->fee_deposit.currency,
&ph->coin_pub,
history,
&h_denom_pub,
&total))
{
GNUNET_break_op (0);
hr.http_status = 0;
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
}
else
{
hr.ec = TALER_JSON_get_error_code (j);
hr.hint = TALER_JSON_get_error_hint (j);
}
ec = TALER_JSON_get_error_code (j);
switch (ec)
{
case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
if (0 > TALER_amount_cmp (&total,
&dki->value))
{
/* recoup MAY have still been possible */
/* FIXME: This code may falsely complain, as we do not
know that the smallest denomination offered by the
exchange is here. We should look at the key
structure of ph->exchange, and find the smallest
_currently withdrawable_ denomination and check
if the value remaining would suffice... */
GNUNET_break_op (0);
hr.http_status = 0;
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
break;
}
break;
case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
if (0 == GNUNET_memcmp (&ph->pk.h_key,
&h_denom_pub))
{
/* invalid proof provided */
GNUNET_break_op (0);
hr.http_status = 0;
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
break;
}
/* valid error from exchange */
break;
default:
GNUNET_break_op (0);
hr.http_status = 0;
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
break;
}
ph->cb (ph->cb_cls,
&hr,
NULL);
TALER_EXCHANGE_recoup_refresh_cancel (ph);
return;
}
case MHD_HTTP_FORBIDDEN:
/* Nothing really to verify, exchange says one of the signatures is
invalid; as we checked them, this should never happen, we
should pass the JSON reply to the application */
hr.ec = TALER_JSON_get_error_code (j);
hr.hint = TALER_JSON_get_error_hint (j);
break;
case MHD_HTTP_NOT_FOUND:
/* Nothing really to verify, this should never
happen, we should pass the JSON reply to the application */
hr.ec = TALER_JSON_get_error_code (j);
hr.hint = TALER_JSON_get_error_hint (j);
break;
case MHD_HTTP_GONE:
/* Kind of normal: the money was already sent to the merchant
(it was too late for the refund). */
hr.ec = TALER_JSON_get_error_code (j);
hr.hint = TALER_JSON_get_error_hint (j);
break;
case MHD_HTTP_INTERNAL_SERVER_ERROR:
/* Server had an internal issue; we should retry, but this API
leaves this to the application */
hr.ec = TALER_JSON_get_error_code (j);
hr.hint = TALER_JSON_get_error_hint (j);
break;
default:
/* unexpected response code */
hr.ec = TALER_JSON_get_error_code (j);
hr.hint = TALER_JSON_get_error_hint (j);
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u/%d for exchange recoup\n",
(unsigned int) response_code,
(int) hr.ec);
GNUNET_break (0);
break;
}
ph->cb (ph->cb_cls,
&hr,
NULL);
TALER_EXCHANGE_recoup_refresh_cancel (ph);
}
struct TALER_EXCHANGE_RecoupRefreshHandle *
TALER_EXCHANGE_recoup_refresh (
struct TALER_EXCHANGE_Handle *exchange,
const struct TALER_EXCHANGE_DenomPublicKey *pk,
const struct TALER_DenominationSignature *denom_sig,
const struct TALER_PlanchetSecretsP *ps,
TALER_EXCHANGE_RecoupRefreshResultCallback recoup_cb,
void *recoup_cb_cls)
{
struct TALER_EXCHANGE_RecoupRefreshHandle *ph;
struct GNUNET_CURL_Context *ctx;
struct TALER_CoinSpendSignatureP coin_sig;
struct TALER_CoinSpendPublicKeyP coin_pub;
struct TALER_DenominationHash h_denom_pub;
json_t *recoup_obj;
CURL *eh;
char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
GNUNET_assert (GNUNET_YES ==
TEAH_handle_is_ready (exchange));
GNUNET_CRYPTO_eddsa_key_get_public (&ps->coin_priv.eddsa_priv,
&coin_pub.eddsa_pub);
TALER_denom_pub_hash (&pk->key,
&h_denom_pub);
TALER_wallet_recoup_refresh_sign (&h_denom_pub,
&ps->blinding_key,
&ps->coin_priv,
&coin_sig);
recoup_obj = GNUNET_JSON_PACK (
GNUNET_JSON_pack_data_auto ("denom_pub_hash",
&h_denom_pub),
TALER_JSON_pack_denom_sig ("denom_sig",
denom_sig),
GNUNET_JSON_pack_data_auto ("coin_sig",
&coin_sig),
GNUNET_JSON_pack_data_auto ("coin_blind_key_secret",
&ps->blinding_key));
{
char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
char *end;
end = GNUNET_STRINGS_data_to_string (
&coin_pub,
sizeof (struct TALER_CoinSpendPublicKeyP),
pub_str,
sizeof (pub_str));
*end = '\0';
GNUNET_snprintf (arg_str,
sizeof (arg_str),
"/coins/%s/recoup-refresh",
pub_str);
}
ph = GNUNET_new (struct TALER_EXCHANGE_RecoupRefreshHandle);
ph->coin_pub = coin_pub;
ph->exchange = exchange;
ph->pk = *pk;
memset (&ph->pk.key,
0,
sizeof (ph->pk.key)); /* zero out, as lifetime cannot be warranted */
ph->cb = recoup_cb;
ph->cb_cls = recoup_cb_cls;
ph->url = TEAH_path_to_url (exchange,
arg_str);
if (NULL == ph->url)
{
json_decref (recoup_obj);
GNUNET_free (ph);
return NULL;
}
eh = TALER_EXCHANGE_curl_easy_get_ (ph->url);
if ( (NULL == eh) ||
(GNUNET_OK !=
TALER_curl_easy_post (&ph->ctx,
eh,
recoup_obj)) )
{
GNUNET_break (0);
if (NULL != eh)
curl_easy_cleanup (eh);
json_decref (recoup_obj);
GNUNET_free (ph->url);
GNUNET_free (ph);
return NULL;
}
json_decref (recoup_obj);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"URL for recoup-refresh: `%s'\n",
ph->url);
ctx = TEAH_handle_to_context (exchange);
ph->job = GNUNET_CURL_job_add2 (ctx,
eh,
ph->ctx.headers,
&handle_recoup_refresh_finished,
ph);
return ph;
}
void
TALER_EXCHANGE_recoup_refresh_cancel (
struct TALER_EXCHANGE_RecoupRefreshHandle *ph)
{
if (NULL != ph->job)
{
GNUNET_CURL_job_cancel (ph->job);
ph->job = NULL;
}
GNUNET_free (ph->url);
TALER_curl_easy_post_finished (&ph->ctx);
GNUNET_free (ph);
}
/* end of exchange_api_recoup_refresh.c */