aboutsummaryrefslogtreecommitdiff
path: root/src/exchange/taler-exchange-httpd_age-withdraw.c
diff options
context:
space:
mode:
authorÖzgür Kesim <oec-taler@kesim.org>2023-06-26 00:01:31 +0200
committerÖzgür Kesim <oec-taler@kesim.org>2023-06-26 00:01:31 +0200
commitddedf03a816e5139b235a3ebdf5b600508c5ed5f (patch)
treea65179048fc764ec82ddf645a8982186b0157448 /src/exchange/taler-exchange-httpd_age-withdraw.c
parent70bfe0ed1b9a5dbb6cc487465ef3c3df4cdb0436 (diff)
[age-withdraw] age-withdraw commit- and reveal-handlers implemented, 12/n
The handlers for the commit- and reveal-phases of the age-withdraw HTTP-endpoints are implemented, yet not active. Still missing: - support for age-withdraw is missing in lib/. - tests
Diffstat (limited to 'src/exchange/taler-exchange-httpd_age-withdraw.c')
-rw-r--r--src/exchange/taler-exchange-httpd_age-withdraw.c933
1 files changed, 775 insertions, 158 deletions
diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.c b/src/exchange/taler-exchange-httpd_age-withdraw.c
index d9475e8f..e7f8b53f 100644
--- a/src/exchange/taler-exchange-httpd_age-withdraw.c
+++ b/src/exchange/taler-exchange-httpd_age-withdraw.c
@@ -22,8 +22,13 @@
* @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"
@@ -31,6 +36,502 @@
#include "taler-exchange-httpd_responses.h"
#include "taler-exchange-httpd_keys.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;
+
+ /**
+ * kappa * #num_coins hashes of blinded coin planchets.
+ */
+ struct TALER_BlindedPlanchet *coin_evs;
+
+ /**
+ * #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);
+ /* commitment.denom_serials and .h_coin_evs are stack allocated */
+}
+
+
+/**
+ * Parse the denominations and blinded coin data of an '/age-withdraw' request.
+ *
+ * @param connection The MHD connection to handle
+ * @param j_denoms_h 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] aws 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_denoms_h,
+ 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;
+
+
+ /* 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_denoms_h);
+
+ if (! json_is_array (j_denoms_h))
+ 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;
+ }
+
+ /* Continue parsing the parts */
+
+ /* Parse denomination keys */
+ awc->denom_hs = GNUNET_new_array (awc->num_coins,
+ struct TALER_DenominationHashP);
+
+ json_array_foreach (j_denoms_h, 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;
+ }
+ };
+
+ /* no overflow because
+ * num_coins <= TALER_MAX_FRESH_COINS
+ * and
+ * TALER_MAX_FRESH_COINS * TALER_CNC_KAPPA < INT_MAX
+ */
+ awc->coin_evs = GNUNET_new_array (awc->num_coins * TALER_CNC_KAPPA,
+ struct TALER_BlindedPlanchet);
+
+ /* Parse blinded envelopes. */
+ json_array_foreach (j_blinded_coin_evs, idx, value) {
+ const json_t *j_kappa_coin_evs;
+ struct GNUNET_JSON_Specification aspec[] = {
+ GNUNET_JSON_spec_array_const (NULL, &j_kappa_coin_evs),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (value, aspec, NULL, NULL))
+ {
+ GNUNET_snprintf (buf,
+ sizeof(buf),
+ "couldn't parse entry no. %d in array coin_evs",
+ idx + 1);
+ error = buf;
+ goto EXIT;
+ }
+
+ 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 */
+ {
+ size_t off = idx * TALER_CNC_KAPPA;
+ size_t kappa = 0;
+
+ json_array_foreach (j_kappa_coin_evs, kappa, value) {
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_fixed_auto (NULL, &awc->coin_evs[off + 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 coin_evs",
+ idx + 1);
+ error = buf;
+ goto EXIT;
+ }
+
+ /* Check for duplicate planchets
+ * FIXME: is this needed?
+ */
+ for (unsigned int i = 0; i < off + kappa; i++)
+ {
+ if (0 == TALER_blinded_planchet_cmp (&awc->coin_evs[off + kappa],
+ &awc->coin_evs[i]))
+ {
+ error = "duplicate planchet";
+ goto EXIT;
+ }
+ }
+ }
+ }
+ }; /* json_array_foreach over j_blinded_coin_evs */
+
+ /* We successfully parsed denoms_h and blinded_coins_evs */
+ GNUNET_assert (NULL == error);
+
+ /* Finally, calculate the h_commitment from all blinded envelopes */
+ {
+ enum GNUNET_GenericReturnValue ret;
+ struct GNUNET_HashContext *hash_context;
+
+ hash_context = GNUNET_CRYPTO_hash_context_start ();
+
+ for (size_t c = 0;
+ c < TALER_CNC_KAPPA * awc->num_coins;
+ c++)
+ {
+ struct TALER_BlindedCoinHashP bch;
+
+ ret = TALER_coin_ev_hash (&awc->coin_evs[c],
+ &awc->denom_hs[c],
+ &bch);
+
+ GNUNET_assert (GNUNET_OK == ret);
+ GNUNET_CRYPTO_hash_context_read (hash_context,
+ &bch,
+ sizeof(bch));
+ }
+
+ GNUNET_CRYPTO_hash_context_finish (hash_context,
+ &awc->commitment.h_commitment.hash);
+ }
+
+
+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] dk 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
+ */
+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 denoms_h array of hashes of denomination public keys
+ * @param coin_evs array of blinded coin planchets
+ * @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 succes, 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,
+ 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;
+
+ GNUNET_assert (*denom_serials == NULL);
+
+ 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);
+
+ TALER_amount_set_zero (TEH_currency, &total_amount);
+ 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' */
+ if (dk->denom_pub.cipher != coin_evs[i].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,57 +573,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;
-
- /**
- * Hash of the wire source URL, needed when kyc is needed.
- */
- struct TALER_PaytoHashP h_payto;
-
- /**
- * The data from the age-withdraw request
- */
- struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment;
+ enum GNUNET_DB_QueryStatus qs;
+ struct TALER_EXCHANGEDB_AgeWithdraw commitment;
- /**
- * Number of coins/denonations in the reveal
- */
- uint32_t num_coins;
+ 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. */
+ }
- /**
- * kappa * #num_coins hashes of blinded coin planchets.
- */
- struct TALER_BlindedPlanchet *coin_evs;
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ return false;
- /**
- * #num_coins hashes of the denominations from which the coins are withdrawn.
- * Those must support age restriction.
- */
- struct TALER_DenominationHashP *denoms_h;
-};
+ /* 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,
@@ -162,9 +668,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,
@@ -178,20 +681,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;
@@ -210,17 +710,16 @@ 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,
@@ -231,37 +730,77 @@ age_withdraw_transaction (void *cls,
}
awc->kyc.ok = true;
- qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls,
- &awc->commitment,
- &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;
+ uint64_t ruuid;
+
+ qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls,
+ &awc->commitment,
+ awc->now,
+ &found,
+ &balance_ok,
+ &age_ok,
+ &allowed_maximum_age,
+ &conflict,
+ &ruuid);
+ 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;
+ }
+ else if (! found)
+ {
+ *mhd_ret = TALER_MHD_reply_with_ec (connection,
+ TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
+ NULL);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ else 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,
+ &awc->commitment.amount_with_fee,
+ &awc->commitment.reserve_pub);
+
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ else if (! age_ok)
+ {
+ enum TALER_ErrorCode ec =
+ TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE;
+
+ *mhd_ret =
+ TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ TALER_ErrorCode_get_http_status_safe (ec),
+ TALER_MHD_PACK_EC (ec),
+ GNUNET_JSON_pack_uint64 ("allowed_maximum_age",
+ allowed_maximum_age));
+
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ else 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;
+ }
}
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
@@ -271,48 +810,107 @@ 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 h_commitment Original commitment
+ * @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[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 */
+ awc->commitment.noreveal_index =
+ noreveal_index =
+ GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG,
+ TALER_CNC_KAPPA);
+
+ /* 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[TALER_CNC_KAPPA * i + noreveal_index];
+ csds[i].h_denom_pub = &awc->denom_hs[i];
+ }
+
+ ec = TEH_keys_denomination_batch_sign (csds,
+ awc->num_coins,
+ 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],
+ &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);
+
+ if (GNUNET_OK != ret)
+ {
+ GNUNET_break (0);
+ *result = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR,
+ NULL);
+ }
+
+ /* Free resources */
+ awc->commitment.h_coin_evs = NULL;
+ awc->commitment.denom_sigs = NULL;
+ for (unsigned int i = 0; i<awc->num_coins; i++)
+ TALER_blinded_denom_sig_free (&denom_sigs[i]);
+ return ret;
}
@@ -322,17 +920,18 @@ TEH_handler_age_withdraw (struct TEH_RequestContext *rc,
const json_t *root)
{
MHD_RESULT mhd_ret;
+ const json_t *j_denoms_h;
+ const json_t *j_blinded_coins_evs;
struct AgeWithdrawContext awc = {0};
struct GNUNET_JSON_Specification spec[] = {
- 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_array_const ("denoms_h",
+ &j_denoms_h),
+ GNUNET_JSON_spec_array_const ("blinded_coins_evs",
+ &j_blinded_coins_evs),
GNUNET_JSON_spec_uint16 ("max_age",
&awc.commitment.max_age),
+ GNUNET_JSON_spec_fixed_auto ("reserve_sig",
+ &awc.commitment.reserve_sig),
GNUNET_JSON_spec_end ()
};
@@ -351,53 +950,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_denoms_h,
+ j_blinded_coins_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);
GNUNET_JSON_parse_free (spec);
+ free_age_withdraw_context_resources (&awc);
return mhd_ret;
}