/* This file is part of TALER Copyright (C) 2023 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-exchange-httpd_age-withdraw.c * @brief Handle /reserves/$RESERVE_PUB/age-withdraw requests * @author Özgür Kesim */ #include "platform.h" #include #include #include #include #include #include "taler-exchange-httpd.h" #include "taler_error_codes.h" #include "taler_json_lib.h" #include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_age-withdraw.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_keys.h" #include "taler_util.h" /** * Context for #age_withdraw_transaction. */ struct AgeWithdrawContext { /** * KYC status for the operation. */ struct TALER_EXCHANGEDB_KycStatus kyc; /** * Timestamp */ struct GNUNET_TIME_Timestamp now; /** * Hash of the wire source URL, needed when kyc is needed. */ struct TALER_PaytoHashP h_payto; /** * The data from the age-withdraw request, as we persist it */ struct TALER_EXCHANGEDB_AgeWithdraw commitment; /** * Number of coins/denonations in the reveal */ uint32_t num_coins; /** * #num_coins * #kappa hashes of blinded coin planchets. */ struct TALER_BlindedPlanchet (*coin_evs) [ TALER_CNC_KAPPA]; /** * #num_coins hashes of the denominations from which the coins are withdrawn. * Those must support age restriction. */ struct TALER_DenominationHashP *denom_hs; }; /* * @brief Free the resources within a AgeWithdrawContext * * @param awc the context to free */ static void free_age_withdraw_context_resources (struct AgeWithdrawContext *awc) { GNUNET_free (awc->denom_hs); GNUNET_free (awc->coin_evs); GNUNET_free (awc->commitment.denom_serials); /* * Note: * awc->commitment.denom_sigs and .h_coin_evs were stack allocated and * .denom_pub_hashes is NULL for this context. */ } /** * Parse the denominations and blinded coin data of an '/age-withdraw' request. * * @param connection The MHD connection to handle * @param j_denom_hs Array of n hashes of the denominations for the withdrawal, in JSON format * @param j_blinded_coin_evs Array of n arrays of kappa blinded envelopes of in JSON format for the coins. * @param[out] awc The context of the operation, only partially built at call time * @param[out] mhd_ret The result if a reply is queued for MHD * @return true on success, false on failure, with a reply already queued for MHD */ static enum GNUNET_GenericReturnValue parse_age_withdraw_json ( struct MHD_Connection *connection, const json_t *j_denom_hs, const json_t *j_blinded_coin_evs, struct AgeWithdrawContext *awc, MHD_RESULT *mhd_ret) { char buf[256] = {0}; const char *error = NULL; unsigned int idx = 0; json_t *value = NULL; struct GNUNET_HashContext *hash_context; /* The age value MUST be on the beginning of an age group */ if (awc->commitment.max_age != TALER_get_lowest_age (&TEH_age_restriction_config.mask, awc->commitment.max_age)) { error = "max_age must be the lower edge of an age group"; goto EXIT; } /* Verify JSON-structure consistency */ { uint32_t num_coins = json_array_size (j_denom_hs); if (! json_is_array (j_denom_hs)) error = "denoms_h must be an array"; else if (! json_is_array (j_blinded_coin_evs)) error = "coin_evs must be an array"; else if (num_coins == 0) error = "denoms_h must not be empty"; else if (num_coins != json_array_size (j_blinded_coin_evs)) error = "denoms_h and coins_evs must be arrays of the same size"; else if (num_coins > TALER_MAX_FRESH_COINS) /** * The wallet had committed to more than the maximum coins allowed, the * reserve has been charged, but now the user can not withdraw any money * from it. Note that the user can't get their money back in this case! **/ error = "maximum number of coins that can be withdrawn has been exceeded"; _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA), "TALER_MAX_FRESH_COINS too large"); if (NULL != error) goto EXIT; awc->num_coins = num_coins; awc->commitment.num_coins = num_coins; } /* Continue parsing the parts */ /* Parse denomination keys */ awc->denom_hs = GNUNET_new_array (awc->num_coins, struct TALER_DenominationHashP); json_array_foreach (j_denom_hs, idx, value) { struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto (NULL, &awc->denom_hs[idx]), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (value, spec, NULL, NULL)) { GNUNET_snprintf (buf, sizeof(buf), "couldn't parse entry no. %d in array denoms_h", idx + 1); error = buf; goto EXIT; } }; { typedef struct TALER_BlindedPlanchet _array_of_kappa_planchets[TALER_CNC_KAPPA]; awc->coin_evs = GNUNET_new_array (awc->num_coins, _array_of_kappa_planchets); } hash_context = GNUNET_CRYPTO_hash_context_start (); GNUNET_assert (NULL != hash_context); /* Parse blinded envelopes. */ json_array_foreach (j_blinded_coin_evs, idx, value) { const json_t *j_kappa_coin_evs = value; if (! json_is_array (j_kappa_coin_evs)) { GNUNET_snprintf (buf, sizeof(buf), "enxtry %d in array blinded_coin_evs is not an array", idx + 1); error = buf; goto EXIT; } else if (TALER_CNC_KAPPA != json_array_size (j_kappa_coin_evs)) { GNUNET_snprintf (buf, sizeof(buf), "array no. %d in coin_evs not of correct size", idx + 1); error = buf; goto EXIT; } /* Now parse the individual kappa envelopes and calculate the hash of * the commitment along the way. */ { unsigned int kappa = 0; json_array_foreach (j_kappa_coin_evs, kappa, value) { struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_blinded_planchet (NULL, &awc->coin_evs[idx][kappa]), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (value, spec, NULL, NULL)) { GNUNET_snprintf (buf, sizeof(buf), "couldn't parse array no. %d in blinded_coin_evs[%d]", kappa + 1, idx + 1); error = buf; goto EXIT; } /* Continue to hash of the coin candidates */ { struct TALER_BlindedCoinHashP bch; TALER_coin_ev_hash (&awc->coin_evs[idx][kappa], &awc->denom_hs[idx], &bch); GNUNET_CRYPTO_hash_context_read (hash_context, &bch, sizeof(bch)); } /* Check for duplicate planchets. Technically a bug on * the client side that is harmless for us, but still * not allowed per protocol */ for (unsigned int i = 0; i < idx; i++) { if (0 == TALER_blinded_planchet_cmp (&awc->coin_evs[idx][kappa], &awc->coin_evs[i][kappa])) { GNUNET_JSON_parse_free (spec); error = "duplicate planchet"; goto EXIT; } } } } }; /* json_array_foreach over j_blinded_coin_evs */ /* Finally, calculate the h_commitment from all blinded envelopes */ GNUNET_CRYPTO_hash_context_finish (hash_context, &awc->commitment.h_commitment.hash); GNUNET_assert (NULL == error); EXIT: if (NULL != error) { /* Note: resources are freed in caller */ *mhd_ret = TALER_MHD_reply_with_ec ( connection, TALER_EC_GENERIC_PARAMETER_MALFORMED, error); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Check if the given denomination is still or already valid, has not been * revoked and supports age restriction. * * @param connection HTTP-connection to the client * @param ksh The handle to the current state of (denomination) keys in the exchange * @param denom_h Hash of the denomination key to check * @param[out] pdk On success, will contain the denomination key details * @param[out] result On failure, an MHD-response will be queued and result will be set to accordingly * @return true on success (denomination valid), false otherwise */ static bool denomination_is_valid ( struct MHD_Connection *connection, struct TEH_KeyStateHandle *ksh, const struct TALER_DenominationHashP *denom_h, struct TEH_DenominationKey **pdk, MHD_RESULT *result) { struct TEH_DenominationKey *dk; dk = TEH_keys_denomination_by_hash_from_state (ksh, denom_h, connection, result); if (NULL == dk) { /* The denomination doesn't exist */ /* Note: a HTTP-response has been queued and result has been set by * TEH_keys_denominations_by_hash_from_state */ return false; } if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) { /* This denomination is past the expiration time for withdraws */ /* FIXME[oec]: add idempotency check */ *result = TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, denom_h, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "age-withdraw_reveal"); return false; } if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) { /* This denomination is not yet valid */ *result = TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, denom_h, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, "age-withdraw_reveal"); return false; } if (dk->recoup_possible) { /* This denomination has been revoked */ *result = TALER_MHD_reply_with_ec ( connection, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, NULL); return false; } if (0 == dk->denom_pub.age_mask.bits) { /* This denomation does not support age restriction */ char msg[256] = {0}; GNUNET_snprintf (msg, sizeof(msg), "denomination %s does not support age restriction", GNUNET_h2s (&denom_h->hash)); *result = TALER_MHD_reply_with_ec ( connection, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, msg); return false; } *pdk = dk; return true; } /** * Check if the given array of hashes of denomination_keys a) belong * to valid denominations and b) those are marked as age restricted. * Also, calculate the total amount of the denominations including fees * for withdraw. * * @param connection The HTTP connection to the client * @param len The lengths of the array @a denoms_h * @param denom_hs array of hashes of denomination public keys * @param coin_evs array of blinded coin planchet candidates * @param[out] denom_serials On success, will be filled with the serial-id's of the denomination keys. Caller must deallocate. * @param[out] amount_with_fee On success, will contain the committed amount including fees * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. * @return #GNUNET_OK if the denominations are valid and support age-restriction * #GNUNET_SYSERR otherwise */ static enum GNUNET_GenericReturnValue are_denominations_valid ( struct MHD_Connection *connection, uint32_t len, const struct TALER_DenominationHashP *denom_hs, const struct TALER_BlindedPlanchet (*coin_evs) [ TALER_CNC_KAPPA], uint64_t **denom_serials, struct TALER_Amount *amount_with_fee, MHD_RESULT *result) { struct TALER_Amount total_amount; struct TALER_Amount total_fee; struct TEH_KeyStateHandle *ksh; uint64_t *serials; ksh = TEH_keys_get_state (); if (NULL == ksh) { *result = TALER_MHD_reply_with_ec (connection, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); return GNUNET_SYSERR; } *denom_serials = serials = GNUNET_new_array (len, uint64_t); GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &total_amount)); GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &total_fee)); for (uint32_t i = 0; i < len; i++) { struct TEH_DenominationKey *dk; if (! denomination_is_valid (connection, ksh, &denom_hs[i], &dk, result)) /* FIXME[oec]: add idempotency check */ return GNUNET_SYSERR; /* Ensure the ciphers from the planchets match the denominations' */ for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) { if (dk->denom_pub.bsign_pub_key->cipher != coin_evs[i][k].blinded_message->cipher) { GNUNET_break_op (0); *result = TALER_MHD_reply_with_ec (connection, TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, NULL); return GNUNET_SYSERR; } } /* Accumulate the values */ if (0 > TALER_amount_add (&total_amount, &total_amount, &dk->meta.value)) { GNUNET_break_op (0); *result = TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, "amount"); return GNUNET_SYSERR; } /* Accumulate the withdraw fees */ if (0 > TALER_amount_add (&total_fee, &total_fee, &dk->meta.fees.withdraw)) { GNUNET_break_op (0); *result = TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, "fee"); return GNUNET_SYSERR; } serials[i] = dk->meta.serial; } /* Save the total amount including fees */ GNUNET_assert (0 < TALER_amount_add (amount_with_fee, &total_amount, &total_fee)); return GNUNET_OK; } /** * @brief Verify the signature of the request body with the reserve key * * @param connection the connection to the client * @param commitment the age withdraw commitment * @param mhd_ret the response to fill in the error case * @return GNUNET_OK on success */ static enum GNUNET_GenericReturnValue verify_reserve_signature ( struct MHD_Connection *connection, const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, enum MHD_Result *mhd_ret) { TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_age_withdraw_verify (&commitment->h_commitment, &commitment->amount_with_fee, &TEH_age_restriction_config.mask, commitment->max_age, &commitment->reserve_pub, &commitment->reserve_sig)) { GNUNET_break_op (0); *mhd_ret = TALER_MHD_reply_with_ec (connection, TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, NULL); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Send a response to a "age-withdraw" request. * * @param connection the connection to send the response to * @param ach value the client committed to * @param noreveal_index which index will the client not have to reveal * @return a MHD status code */ static MHD_RESULT reply_age_withdraw_success ( struct MHD_Connection *connection, const struct TALER_AgeWithdrawCommitmentHashP *ach, uint32_t noreveal_index) { struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; enum TALER_ErrorCode ec = TALER_exchange_online_age_withdraw_confirmation_sign ( &TEH_keys_exchange_sign_, ach, noreveal_index, &pub, &sig); if (TALER_EC_NONE != ec) return TALER_MHD_reply_with_ec (connection, ec, NULL); return TALER_MHD_REPLY_JSON_PACK (connection, MHD_HTTP_OK, GNUNET_JSON_pack_uint64 ("noreveal_index", noreveal_index), GNUNET_JSON_pack_data_auto ("exchange_sig", &sig), GNUNET_JSON_pack_data_auto ("exchange_pub", &pub)); } /** * 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 */ static bool request_is_idempotent (struct MHD_Connection *con, struct AgeWithdrawContext *awc, MHD_RESULT *mret) { enum GNUNET_DB_QueryStatus qs; struct TALER_EXCHANGEDB_AgeWithdraw commitment; 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. */ } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return false; /* 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 * 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, of type struct AgeWithdrawContext */ static void age_withdraw_amount_cb (void *cls, struct GNUNET_TIME_Absolute limit, TALER_EXCHANGEDB_KycAmountCallback cb, void *cb_cls) { struct AgeWithdrawContext *awc = cls; enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Signaling amount %s for KYC check during age-withdrawal\n", TALER_amount2s (&awc->commitment.amount_with_fee)); if (GNUNET_OK != cb (cb_cls, &awc->commitment.amount_with_fee, awc->now.abs_time)) return; qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (TEH_plugin->cls, &awc->h_payto, limit, cb, cb_cls); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Got %d additional transactions for this age-withdrawal and limit %llu\n", qs, (unsigned long long) limit.abs_value_us); GNUNET_break (qs >= 0); } /** * Function implementing age withdraw transaction. Runs the * transaction logic; IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns an hard error, * the transaction logic MUST queue a MHD response and set @a mhd_ret. * IF it returns the soft error code, the function MAY be called again * to retry and MUST not queue a MHD response. * * @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, * if transaction failed (!) * @return transaction status */ static enum GNUNET_DB_QueryStatus age_withdraw_transaction (void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { struct AgeWithdrawContext *awc = cls; enum GNUNET_DB_QueryStatus qs; 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, in which case no KYC check is required as the merge already did that. */ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { char *kyc_required; qs = TALER_KYCLOGIC_kyc_test_required ( TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW, &awc->h_payto, TEH_plugin->select_satisfied_kyc_processes, TEH_plugin->cls, &age_withdraw_amount_cb, awc, &kyc_required); if (qs < 0) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); *mhd_ret = TALER_MHD_reply_with_ec (connection, TALER_EC_GENERIC_DB_FETCH_FAILED, "kyc_test_required"); } return qs; } if (NULL != kyc_required) { /* Mark result and return by inserting KYC requirement into DB! */ awc->kyc.ok = false; return TEH_plugin->insert_kyc_requirement_for_account ( TEH_plugin->cls, kyc_required, &awc->h_payto, &awc->commitment.reserve_pub, &awc->kyc.requirement_row); } } awc->kyc.ok = true; /* KYC requirement fulfilled, do the age-withdraw transaction */ { bool found = false; bool balance_ok = false; bool age_ok = false; bool conflict = false; uint16_t allowed_maximum_age = 0; uint32_t reserve_birthday = 0; struct TALER_Amount reserve_balance; qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls, &awc->commitment, awc->now, &found, &balance_ok, &reserve_balance, &age_ok, &allowed_maximum_age, &reserve_birthday, &conflict); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) *mhd_ret = TALER_MHD_reply_with_ec (connection, TALER_EC_GENERIC_DB_FETCH_FAILED, "do_age_withdraw"); return qs; } if (! found) { *mhd_ret = TALER_MHD_reply_with_ec (connection, TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, NULL); return GNUNET_DB_STATUS_HARD_ERROR; } if (! age_ok) { enum TALER_ErrorCode ec = TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE; *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_CONFLICT, TALER_MHD_PACK_EC (ec), GNUNET_JSON_pack_uint64 ("allowed_maximum_age", allowed_maximum_age), GNUNET_JSON_pack_uint64 ("reserve_birthday", reserve_birthday)); return GNUNET_DB_STATUS_HARD_ERROR; } if (! balance_ok) { TEH_plugin->rollback (TEH_plugin->cls); *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( connection, TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS, &reserve_balance, &awc->commitment.amount_with_fee, &awc->commitment.reserve_pub); return GNUNET_DB_STATUS_HARD_ERROR; } if (conflict) { /* do_age_withdraw signaled a conflict, so there MUST be an entry * in the DB. Put that into the response */ bool ok = request_is_idempotent (connection, awc, mhd_ret); GNUNET_assert (ok); return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } *mhd_ret = -1; } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW]++; return qs; } /** * @brief Sign the chosen blinded coins, debit the reserve and persist * the commitment. * * On conflict, the noreveal_index from the previous, existing * commitment is returned to the client, returning success. * * On error (like, insufficient funds), the client is notified. * * Note that on success, there are two possible states: * 1.) KYC is required (awc.kyc.ok == false) or * 2.) age withdraw was successful. * * @param connection HTTP-connection to the client * @param awc The context for the current age withdraw request * @param[out] result On error, a HTTP-response will be queued and result set accordingly * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise */ static enum GNUNET_GenericReturnValue sign_and_do_age_withdraw ( struct MHD_Connection *connection, struct AgeWithdrawContext *awc, MHD_RESULT *result) { 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; awc->now = GNUNET_TIME_timestamp_get (); /* Pick the challenge */ noreveal_index = GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, TALER_CNC_KAPPA); awc->commitment.noreveal_index = noreveal_index; /* Choose and sign the coins */ { struct TEH_CoinSignData csds[awc->num_coins]; enum TALER_ErrorCode ec; /* Pick the chosen blinded coins */ for (uint32_t i = 0; inum_coins; i++) { csds[i].bp = &awc->coin_evs[i][noreveal_index]; csds[i].h_denom_pub = &awc->denom_hs[i]; } ec = TEH_keys_denomination_batch_sign (awc->num_coins, csds, false, denom_sigs); if (TALER_EC_NONE != ec) { GNUNET_break (0); *result = TALER_MHD_reply_with_ec (connection, ec, NULL); return GNUNET_SYSERR; } } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Signatures ready, starting DB interaction\n"); /* Prepare the hashes of the coins for insertion */ for (uint32_t i = 0; inum_coins; i++) { TALER_coin_ev_hash (&awc->coin_evs[i][noreveal_index], &awc->denom_hs[i], &h_coin_evs[i]); } /* Run the transaction */ awc->commitment.h_coin_evs = h_coin_evs; awc->commitment.denom_sigs = denom_sigs; ret = TEH_DB_run_transaction (connection, "run age withdraw", TEH_MT_REQUEST_AGE_WITHDRAW, result, &age_withdraw_transaction, awc); /* Free resources */ for (unsigned int i = 0; inum_coins; i++) TALER_blinded_denom_sig_free (&denom_sigs[i]); awc->commitment.h_coin_evs = NULL; awc->commitment.denom_sigs = NULL; return ret; } MHD_RESULT TEH_handler_age_withdraw (struct TEH_RequestContext *rc, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *root) { MHD_RESULT mhd_ret; const json_t *j_denom_hs; const json_t *j_blinded_coin_evs; struct AgeWithdrawContext awc = {0}; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_array_const ("denom_hs", &j_denom_hs), GNUNET_JSON_spec_array_const ("blinded_coin_evs", &j_blinded_coin_evs), GNUNET_JSON_spec_uint16 ("max_age", &awc.commitment.max_age), GNUNET_JSON_spec_fixed_auto ("reserve_sig", &awc.commitment.reserve_sig), GNUNET_JSON_spec_end () }; awc.commitment.reserve_pub = *reserve_pub; /* Parse the JSON body */ { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (rc->connection, root, spec); if (GNUNET_OK != res) return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; } do { /* Note: If we break the statement here at any point, * a response to the client MUST have been populated * with an appropriate answer and mhd_ret MUST have * been set accordingly. */ /* Parse denoms_h and blinded_coins_evs, partially fill awc */ if (GNUNET_OK != parse_age_withdraw_json (rc->connection, j_denom_hs, j_blinded_coin_evs, &awc, &mhd_ret)) break; /* Ensure validity of denoms and calculate amounts and fees */ if (GNUNET_OK != 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; /* Now that amount_with_fee is calculated, verify the signature of * the request body with the reserve key. */ if (GNUNET_OK != verify_reserve_signature (rc->connection, &awc.commitment, &mhd_ret)) break; /* 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) 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); } while (0); GNUNET_JSON_parse_free (spec); free_age_withdraw_context_resources (&awc); return mhd_ret; } /* end of taler-exchange-httpd_age-withdraw.c */