diff options
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 |
commit | ddedf03a816e5139b235a3ebdf5b600508c5ed5f (patch) | |
tree | a65179048fc764ec82ddf645a8982186b0157448 /src/exchange | |
parent | 70bfe0ed1b9a5dbb6cc487465ef3c3df4cdb0436 (diff) | |
download | exchange-ddedf03a816e5139b235a3ebdf5b600508c5ed5f.tar.gz exchange-ddedf03a816e5139b235a3ebdf5b600508c5ed5f.tar.bz2 exchange-ddedf03a816e5139b235a3ebdf5b600508c5ed5f.zip |
[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')
-rw-r--r-- | src/exchange/taler-exchange-httpd_age-withdraw.c | 933 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_age-withdraw_reveal.c | 932 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_batch-withdraw.c | 35 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_csr.c | 16 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_extensions.c | 2 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_keys.c | 92 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_keys.h | 4 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_refreshes_reveal.c | 10 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_responses.c | 14 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_responses.h | 14 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_withdraw.c | 11 |
11 files changed, 1130 insertions, 933 deletions
diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.c b/src/exchange/taler-exchange-httpd_age-withdraw.c index d9475e8f6..e7f8b53fc 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; } diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c index d604632d9..9bb6ac401 100644 --- a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c +++ b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c @@ -19,10 +19,12 @@ * @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" @@ -50,95 +52,30 @@ 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 coin planchets. - */ - struct TALER_BlindedPlanchet *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; }; /** - * Information per planchet in the batch. - */ -struct PlanchetContext -{ - - /** - * Hash of the (blinded) message to be signed by the Exchange. - */ - struct TALER_BlindedCoinHashP h_coin_envelope; - - /** - * Value of the coin being exchanged (matching the denomination key) - * plus the transaction fee. We include this in what is being - * signed so that we can verify a reserve's remaining total balance - * without needing to access the respective denomination key - * information each time. - */ - struct TALER_Amount amount_with_fee; - - /** - * Blinded planchet. - */ - struct TALER_BlindedPlanchet blinded_planchet; - - /** - * Set to the resulting signed coin data to be returned to the client. - */ - struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; - -}; - -/** - * 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 * the denomination hashes, blinded coins and disclosed coins and allocates * memory for those. @@ -154,50 +91,40 @@ 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 * (TALER_CNC_KAPPA - 1)) 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"; + else if (0 != num_entries % (TALER_CNC_KAPPA - 1)) + error = "the size of disclosed_coin_secrets must be a multiple of " + TALER_CNC_KAPPA_MINUS_ONE_STR; 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; + actx->num_coins = num_entries / (TALER_CNC_KAPPA - 1); + } /* Continue parsing the parts */ @@ -205,78 +132,10 @@ parse_age_withdraw_reveal_json ( unsigned int idx = 0; 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 () - }; - - 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 denoms_h", - idx + 1); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); - goto EXIT; - } - }; - - /* Parse blinded envelopes */ - actx->coin_evs = GNUNET_new_array (actx->num_coins, - struct TALER_BlindedPlanchet); - - json_array_foreach (j_coin_evs, idx, value) { - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_blinded_planchet (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; - } - - /* Check for duplicate planchets */ - for (unsigned int i = 0; i < idx; i++) - { - if (0 == TALER_blinded_planchet_cmp (&actx->coin_evs[idx], - &actx->coin_evs[i])) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "duplicate planchet"); - goto EXIT; - } - - } - }; - /* Parse diclosed keys */ - actx->disclosed_coin_secrets = GNUNET_new_array ( - actx->num_coins * (TALER_CNC_KAPPA - 1), - struct TALER_PlanchetMasterSecretP); + actx->disclosed_coin_secrets = + GNUNET_new_array (num_entries, + struct TALER_PlanchetMasterSecretP); json_array_foreach (j_disclosed_coin_secrets, idx, value) { struct GNUNET_JSON_Specification spec[] = { @@ -292,18 +151,15 @@ parse_age_withdraw_reveal_json ( 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); + *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; @@ -328,256 +184,181 @@ 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 */ + 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); - break; + 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_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); - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - } + 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 unsuccessfull 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 ksh 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] hc 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)) - { - /* 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 (dks->recoup_possible) - { - /* This denomination has been revoked */ - *result = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_GONE, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - NULL); - return false; - } + 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) + return GNUNET_SYSERR; - if (0 == dks->denom_pub.age_mask.bits) + /* calculate age commitment hash */ { - /* 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; - } + struct TALER_AgeCommitmentProof acp; + ret = TALER_age_restriction_from_secret (secret, + &denom_key->denom_pub.age_mask, + max_age, + &acp); - 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 coin_evs array of blinded coin planchets - * @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, - const struct TALER_BlindedPlanchet *coin_evs, - 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); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, /* FIXME[oec]: better error code */ + "derivation of age restriction failed"); + return ret; + } - 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; + TALER_age_commitment_hash (&acp.commitment, &ach); } - *dks = GNUNET_new_array (len, struct TEH_DenominationKey); - TALER_amount_set_zero (TEH_currency, total_amount); - TALER_amount_set_zero (TEH_currency, total_fee); - - for (uint32_t i = 0; i < len; i++) + /* Next: calculate planchet */ { - if (! denomination_is_valid (connection, - ksh, - &denoms_h[i], - dks[i], - result)) - return GNUNET_SYSERR; + struct TALER_CoinPubHashP c_hash; + struct TALER_PlanchetDetail detail; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union TALER_DenominationBlindingKeyP bks; + struct TALER_ExchangeWithdrawValues alg_values = { + .cipher = denom_key->denom_pub.cipher, + }; - /* Ensure the ciphers from the planchets match the denominations' */ - if (dks[i]->denom_pub.cipher != coin_evs[i].cipher) + if (TALER_DENOMINATION_CS == alg_values.cipher) { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL); - return GNUNET_SYSERR; - } + struct TALER_CsNonce nonce; - /* Accumulate the values */ - if (0 > TALER_amount_add ( - total_amount, - total_amount, - &dks[i]->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; - } + TALER_cs_withdraw_nonce_derive ( + secret, + &nonce); - /* Accumulate the withdraw fees */ - if (0 > TALER_amount_add ( - total_fee, - total_fee, - &dks[i]->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; + { + enum TALER_ErrorCode ec; + struct TEH_CsDeriveData cdd = { + .h_denom_pub = &denom_key->h_denom_pub, + .nonce = &nonce, + }; + + ec = TEH_keys_denomination_cs_r_pub (&cdd, + false, + &alg_values.details. + cs_values); + /* FIXME Handle error? */ + GNUNET_assert (TALER_EC_NONE == ec); + } } - } - /* Compare the committed amount against the totals */ - { - struct TALER_Amount sum; - TALER_amount_set_zero (TEH_currency, &sum); + TALER_planchet_blinding_secret_create (secret, + &alg_values, + &bks); + + TALER_planchet_setup_coin_priv (secret, + &alg_values, + &coin_priv); - GNUNET_assert (0 < TALER_amount_add ( - &sum, - total_amount, - total_fee)); + ret = TALER_planchet_prepare (&denom_key->denom_pub, + &alg_values, + &bks, + &coin_priv, + &ach, + &c_hash, + &detail); - if (0 != TALER_amount_cmp (&sum, amount_with_fee)) + if (GNUNET_OK != ret) { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT, - NULL); - return GNUNET_SYSERR; + GNUNET_break (0); + *result = TALER_MHD_reply_json_pack (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "{ss}", + "details", + "failed to prepare planchet from base key"); + return ret; } + + ret = TALER_coin_ev_hash (&detail.blinded_planchet, + &denom_key->h_denom_pub, + bch); + GNUNET_assert (GNUNET_OK == ret); } - return GNUNET_OK; + return GNUNET_SYSERR; } /** - * 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), @@ -594,163 +375,83 @@ 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 blindet planchets of the undisclosed 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_BlindedPlanchet *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<TALER_CNC_KAPPA; idx++) + for (size_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++) { - if (idx == (size_t) noreveal_idx) + if (gamma == (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 = (TALER_CNC_KAPPA - 1) * 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 (i<2); 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_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); - /* FIXME 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_abort (hash_context); + return GNUNET_SYSERR; } /* Continue the running hash of all coin hashes with the calculated @@ -768,7 +469,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); @@ -788,26 +489,22 @@ verify_commitment_and_max_age ( * @brief Send a response for "/age-withdraw/$RCH/reveal" * * @param connection The http connection to the client to send the response to - * @param num_coins Number of new coins with age restriction for which we reveal data - * @param awrcs array of @a num_coins signatures revealed + * @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, - unsigned int num_coins, - const struct TALER_EXCHANGEDB_AgeWithdrawRevealedCoin *awrcs) + const struct TALER_EXCHANGEDB_AgeWithdraw *commitment) { json_t *list = json_array (); GNUNET_assert (NULL != list); - for (unsigned int index = 0; - index < num_coins; - index++) + for (unsigned int i = 0; i < commitment->num_coins; i++) { json_t *obj = GNUNET_JSON_PACK ( TALER_JSON_pack_blinded_denom_sig ("ev_sig", - &awrcs[index].coin_sig)); + &commitment->denom_sigs[i])); GNUNET_assert (0 == json_array_append_new (list, obj)); @@ -821,160 +518,6 @@ reply_age_withdraw_reveal_success ( } -/** - * @brief Signs and persists the undisclosed coins - * - * @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 enum GNUNET_GenericReturnValue -sign_and_finalize_age_withdraw ( - struct MHD_Connection *connection, - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const uint32_t num_coins, - const struct TALER_BlindedPlanchet *coin_evs, - const struct TEH_DenominationKey *denom_keys, - MHD_RESULT *result) -{ - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - struct TEH_CoinSignData csds[num_coins]; - struct TALER_BlindedDenominationSignature bds[num_coins]; - struct TALER_EXCHANGEDB_AgeWithdrawRevealedCoin awrcs[num_coins]; - enum GNUNET_DB_QueryStatus qs; - - for (uint32_t i = 0; i<num_coins; i++) - { - csds[i].h_denom_pub = &denom_keys[i].h_denom_pub; - csds[i].bp = &coin_evs[i]; - } - - /* Sign the the blinded coins first */ - { - enum TALER_ErrorCode ec; - ec = TEH_keys_denomination_batch_sign (csds, - num_coins, - false, - bds); - if (TALER_EC_NONE != ec) - { - GNUNET_break (0); - *result = TALER_MHD_reply_with_ec (connection, - ec, - NULL); - return GNUNET_SYSERR; - } - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Signatures ready, starting DB interaction\n"); - - /* Prepare the data for insertion */ - for (uint32_t i = 0; i<num_coins; i++) - { - TALER_coin_ev_hash (&coin_evs[i], - csds[i].h_denom_pub, - &awrcs[i].h_coin_ev); - awrcs[i].h_denom_pub = *csds[i].h_denom_pub; - awrcs[i].coin_sig = bds[i]; - } - - /* Persist operation result in DB, transactionally */ - for (unsigned int r = 0; r < MAX_TRANSACTION_COMMIT_RETRIES; r++) - { - bool changed = false; - - /* Transaction start */ - if (GNUNET_OK != - TEH_plugin->start (TEH_plugin->cls, - "insert_age_withdraw_reveal batch")) - { - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - goto cleanup; - } - - qs = TEH_plugin->insert_age_withdraw_reveal (TEH_plugin->cls, - h_commitment, - num_coins, - awrcs); - - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - TEH_plugin->rollback (TEH_plugin->cls); - continue; - } - else if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - TEH_plugin->rollback (TEH_plugin->cls); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_age_withdraw_reveal"); - goto cleanup; - } - - changed = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - - /* Commit the transaction */ - qs = TEH_plugin->commit (TEH_plugin->cls); - if (qs >= 0) - { - if (changed) - TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW_REVEAL]++; - - break; /* success */ - - } - else if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - TEH_plugin->rollback (TEH_plugin->cls); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - goto cleanup; - } - else - { - TEH_plugin->rollback (TEH_plugin->cls); - } - } /* end of retry */ - - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - GNUNET_break (0); - TEH_plugin->rollback (TEH_plugin->cls); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - goto cleanup; - } - - /* Generate final (positive) response */ - ret = reply_age_withdraw_reveal_success (connection, - num_coins, - awrcs); -cleanup: - GNUNET_break (GNUNET_OK != ret); - - /* Free resources */ - for (unsigned int i = 0; i<num_coins; i++) - TALER_blinded_denom_sig_free (&awrcs[i].coin_sig); - return ret; -} - - MHD_RESULT TEH_handler_age_withdraw_reveal ( struct TEH_RequestContext *rc, @@ -984,16 +527,10 @@ TEH_handler_age_withdraw_reveal ( MHD_RESULT result = MHD_NO; enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; struct AgeRevealContext actx = {0}; - const json_t *j_denoms_h; - const json_t *j_coin_evs; const json_t *j_disclosed_coin_secrets; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("reserve_pub", &actx.reserve_pub), - GNUNET_JSON_spec_array_const ("denoms_h", - &j_denoms_h), - GNUNET_JSON_spec_array_const ("coin_evs", - &j_coin_evs), GNUNET_JSON_spec_array_const ("disclosed_coin_secrets", &j_disclosed_coin_secrets), GNUNET_JSON_spec_end () @@ -1017,8 +554,6 @@ TEH_handler_age_withdraw_reveal ( if (GNUNET_OK != parse_age_withdraw_reveal_json ( rc->connection, - j_denoms_h, - j_coin_evs, j_disclosed_coin_secrets, &actx, &result)) @@ -1034,49 +569,24 @@ 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.coin_evs, - &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 ( 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, - &result)) - break; - - /* Finally, sign and persist the coins */ - if (GNUNET_OK != - sign_and_finalize_age_withdraw ( - rc->connection, - &actx.commitment.h_commitment, actx.num_coins, - actx.coin_evs, - actx.denom_keys, &result)) break; + /* Finally, return the signatures */ + result = reply_age_withdraw_reveal_success (rc->connection, + actx.commitment); + } while(0); - age_reveal_context_free (&actx); + GNUNET_free (actx.disclosed_coin_secrets); return result; } diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c index 04a9d7208..7455778ad 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" /** @@ -306,8 +308,10 @@ 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 required_age = 0; char *kyc_required; struct TALER_PaytoHashP reserve_h_payto; @@ -471,9 +475,11 @@ batch_withdraw_transaction (void *cls, wc->now, wc->reserve_pub, &wc->batch_total, - /* TODO[oec]: add parameter for maximum age and [out]parameter for required age */ + TEH_age_restriction_enabled, &found, &balance_ok, + &age_ok, + &required_age, &ruuid); if (0 > qs) { @@ -496,7 +502,20 @@ batch_withdraw_transaction (void *cls, return GNUNET_DB_STATUS_HARD_ERROR; } - /* TODO[oec]: add error handling for age restriction requirements */ + 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, + required_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) { @@ -722,10 +741,12 @@ 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 (wc, diff --git a/src/exchange/taler-exchange-httpd_csr.c b/src/exchange/taler-exchange-httpd_csr.c index 3ceb319cd..64892d363 100644 --- a/src/exchange/taler-exchange-httpd_csr.c +++ b/src/exchange/taler-exchange-httpd_csr.c @@ -127,10 +127,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 ( @@ -262,10 +262,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 ( diff --git a/src/exchange/taler-exchange-httpd_extensions.c b/src/exchange/taler-exchange-httpd_extensions.c index 48c3f4a94..68cc2da55 100644 --- a/src/exchange/taler-exchange-httpd_extensions.c +++ b/src/exchange/taler-exchange-httpd_extensions.c @@ -148,7 +148,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", diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c index b39093ec1..e53f27327 100644 --- a/src/exchange/taler-exchange-httpd_keys.c +++ b/src/exchange/taler-exchange-httpd_keys.c @@ -1364,7 +1364,6 @@ denomination_info_cb ( &dk->h_denom_pub.hash, dk, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - } @@ -1753,7 +1752,7 @@ 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 last_cherry_pick_date 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 @@ -1764,7 +1763,7 @@ wallet_threshold_cb (void *cls, 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, @@ -1778,7 +1777,8 @@ create_krd (struct TEH_KeyStateHandle *ksh, struct TALER_ExchangeSignatureP grouped_exchange_sig; json_t *keys; - GNUNET_assert (! GNUNET_TIME_absolute_is_zero (last_cpd.abs_time)); + 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); @@ -1788,7 +1788,7 @@ create_krd (struct TEH_KeyStateHandle *ksh, 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 */ { @@ -1799,7 +1799,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))) @@ -1820,7 +1820,7 @@ create_krd (struct TEH_KeyStateHandle *ksh, TALER_exchange_online_key_set_sign ( &TEH_keys_exchange_sign2_, ksh, - last_cpd, + last_cherry_pick_date, h_grouped, &grouped_exchange_pub, &grouped_exchange_sig))) @@ -1876,7 +1876,7 @@ 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), + last_cherry_pick_date), GNUNET_JSON_pack_data_auto ("eddsa_pub", &exchange_pub), GNUNET_JSON_pack_data_auto ("eddsa_sig", @@ -2034,7 +2034,7 @@ create_krd (struct TEH_KeyStateHandle *ksh, 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); @@ -2054,14 +2054,17 @@ create_krd (struct TEH_KeyStateHandle *ksh, static enum GNUNET_GenericReturnValue finish_keys_response (struct TEH_KeyStateHandle *ksh) { + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; json_t *recoup; struct SignKeyCtx sctx; json_t *denoms = NULL; json_t *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}; + /* Remember if we have any denomination with age restriction */ + bool has_age_restricted_denomination = false; sctx.signkeys = json_array (); GNUNET_assert (NULL != sctx.signkeys); @@ -2094,7 +2097,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) grouped_denominations = json_array (); GNUNET_assert (NULL != grouped_denominations); - last_cpd = GNUNET_TIME_UNIT_ZERO_TS; + last_cherry_pick_date = GNUNET_TIME_UNIT_ZERO_TS; // FIXME: This block contains the implementation of the DEPRECATED // "denom_pubs" array along with the new grouped "denominations". @@ -2124,13 +2127,13 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) /* heap = min 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. */ @@ -2143,7 +2146,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) if (GNUNET_OK != create_krd (ksh, &hc, - last_cpd, + last_cherry_pick_date, sctx.signkeys, recoup, denoms, @@ -2152,21 +2155,17 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to generate key response data for %s\n", - GNUNET_TIME_timestamp2s (last_cpd)); + GNUNET_TIME_timestamp2s (last_cherry_pick_date)); GNUNET_CRYPTO_hash_context_abort (hash_context); /* 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; + last_cherry_pick_date = dk->meta.start; { json_t *denom; @@ -2264,7 +2263,11 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) int r = json_object_set_new (group->json, "age_mask", json_integer (meta.age_mask.bits)); + GNUNET_assert (0 == r); + + /* 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 */ @@ -2386,7 +2389,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) } 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; @@ -2395,7 +2398,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) if (GNUNET_OK != create_krd (ksh, &hc, - last_cpd, + last_cherry_pick_date, sctx.signkeys, recoup, denoms, @@ -2404,14 +2407,25 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) { 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 { @@ -2419,11 +2433,15 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) "No denomination keys available. Refusing to generate /keys response.\n"); GNUNET_CRYPTO_hash_context_abort (hash_context); } + + ret = GNUNET_OK; + +CLEANUP: json_decref (grouped_denominations); json_decref (sctx.signkeys); json_decref (recoup); json_decref (denoms); - return GNUNET_OK; + return ret; } @@ -2733,16 +2751,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) diff --git a/src/exchange/taler-exchange-httpd_keys.h b/src/exchange/taler-exchange-httpd_keys.h index 8758afb71..d4a578127 100644 --- a/src/exchange/taler-exchange-httpd_keys.h +++ b/src/exchange/taler-exchange-httpd_keys.h @@ -234,8 +234,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); diff --git a/src/exchange/taler-exchange-httpd_refreshes_reveal.c b/src/exchange/taler-exchange-httpd_refreshes_reveal.c index 47926a740..4fb164077 100644 --- a/src/exchange/taler-exchange-httpd_refreshes_reveal.c +++ b/src/exchange/taler-exchange-httpd_refreshes_reveal.c @@ -511,7 +511,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,10 +536,10 @@ 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) && diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index 835a47713..d1636a0af 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.c @@ -1085,6 +1085,20 @@ TEH_RESPONSE_reply_reserve_insufficient_balance ( MHD_RESULT +TEH_RESPONSE_reply_reserve_age_restriction_required ( + struct MHD_Connection *connection, + uint16_t maximum_allowed_age) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_JSON_pack_ec (TALER_EC_EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED), + GNUNET_JSON_pack_uint64 ("maximum_allowed_age", + maximum_allowed_age)); +} + + +MHD_RESULT TEH_RESPONSE_reply_purse_created ( struct MHD_Connection *connection, struct GNUNET_TIME_Timestamp exchange_timestamp, diff --git a/src/exchange/taler-exchange-httpd_responses.h b/src/exchange/taler-exchange-httpd_responses.h index 0db6968f8..a57fa495e 100644 --- a/src/exchange/taler-exchange-httpd_responses.h +++ b/src/exchange/taler-exchange-httpd_responses.h @@ -75,6 +75,20 @@ TEH_RESPONSE_reply_reserve_insufficient_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 diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c index 28addba43..9c8a405cc 100644 --- a/src/exchange/taler-exchange-httpd_withdraw.c +++ b/src/exchange/taler-exchange-httpd_withdraw.c @@ -488,10 +488,13 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, GNUNET_JSON_parse_free (spec); return mret; } - dk = TEH_keys_denomination_by_hash2 (ksh, - &wc.collectable.denom_pub_hash, - NULL, - NULL); + + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + &wc.collectable.denom_pub_hash, + NULL, + NULL); + if (NULL == dk) { if (! check_request_idempotent (rc, |