/* This file is part of TALER Copyright (C) 2022 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_reserves_purse.c * @brief Handle /reserves/$RID/purse requests; parses the POST and JSON and * verifies the coin signature before handing things off * to the database. * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include #include "taler_json_lib.h" #include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_reserves_purse.h" #include "taler-exchange-httpd_responses.h" #include "taler_exchangedb_lib.h" #include "taler-exchange-httpd_keys.h" /** * Closure for #purse_transaction. */ struct ReservePurseContext { /** * Public key of the reserve we are creating a purse for. */ const struct TALER_ReservePublicKeyP *reserve_pub; /** * Fees for the operation. */ const struct TEH_GlobalFee *gf; /** * Signature of the reserve affirming the merge. */ struct TALER_ReserveSignatureP reserve_sig; /** * Purse fee the client is willing to pay. */ struct TALER_Amount purse_fee; /** * Total amount already put into the purse. */ struct TALER_Amount deposit_total; /** * Merge time. */ struct GNUNET_TIME_Timestamp merge_timestamp; /** * Our current time. */ struct GNUNET_TIME_Timestamp exchange_timestamp; /** * Details about an encrypted contract, if any. */ struct TALER_EncryptedContract econtract; /** * Merge key for the purse. */ struct TALER_PurseMergePublicKeyP merge_pub; /** * Merge affirmation by the @e merge_pub. */ struct TALER_PurseMergeSignatureP merge_sig; /** * Signature of the client affiming this request. */ struct TALER_PurseContractSignatureP purse_sig; /** * Fundamental details about the purse. */ struct TEH_PurseDetails pd; /** * Hash of the @e payto_uri. */ struct TALER_PaytoHashP h_payto; /** * KYC status of the operation. */ struct TALER_EXCHANGEDB_KycStatus kyc; /** * Minimum age for deposits into this purse. */ uint32_t min_age; /** * Flags for the operation. */ enum TALER_WalletAccountMergeFlags flags; /** * Do we lack an @e econtract? */ bool no_econtract; }; /** * 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 a `struct ReservePurseContext` * @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 */ static void amount_iterator (void *cls, struct GNUNET_TIME_Absolute limit, TALER_EXCHANGEDB_KycAmountCallback cb, void *cb_cls) { struct ReservePurseContext *rpc = cls; enum GNUNET_DB_QueryStatus qs; cb (cb_cls, &rpc->deposit_total, GNUNET_TIME_absolute_get ()); qs = TEH_plugin->select_merge_amounts_for_kyc_check ( TEH_plugin->cls, &rpc->h_payto, limit, cb, cb_cls); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Got %d additional transactions for this merge and limit %llu\n", qs, (unsigned long long) limit.abs_value_us); GNUNET_break (qs >= 0); } /** * Execute database transaction for /reserves/$PID/purse. 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 ReservePurseContext` * @param connection MHD request context * @param[out] mhd_ret set to MHD status on error * @return transaction status */ static enum GNUNET_DB_QueryStatus purse_transaction (void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { struct ReservePurseContext *rpc = cls; enum GNUNET_DB_QueryStatus qs; char *required; qs = TALER_KYCLOGIC_kyc_test_required ( TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, &rpc->h_payto, TEH_plugin->select_satisfied_kyc_processes, TEH_plugin->cls, &amount_iterator, rpc, &required); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return 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"); return GNUNET_DB_STATUS_HARD_ERROR; } if (NULL != required) { rpc->kyc.ok = false; qs = TEH_plugin->insert_kyc_requirement_for_account ( TEH_plugin->cls, required, &rpc->h_payto, rpc->reserve_pub, &rpc->kyc.requirement_row); GNUNET_free (required); 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_STORE_FAILED, "insert_kyc_requirement_for_account"); } return qs; } rpc->kyc.ok = true; { bool in_conflict = true; /* 1) store purse */ qs = TEH_plugin->insert_purse_request ( TEH_plugin->cls, &rpc->pd.purse_pub, &rpc->merge_pub, rpc->pd.purse_expiration, &rpc->pd.h_contract_terms, rpc->min_age, rpc->flags, &rpc->purse_fee, &rpc->pd.target_amount, &rpc->purse_sig, &in_conflict); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; GNUNET_break (0); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "insert purse request"); return GNUNET_DB_STATUS_HARD_ERROR; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return qs; if (in_conflict) { struct TALER_PurseMergePublicKeyP merge_pub; struct GNUNET_TIME_Timestamp purse_expiration; struct TALER_PrivateContractHashP h_contract_terms; struct TALER_Amount target_amount; struct TALER_Amount balance; struct TALER_PurseContractSignatureP purse_sig; uint32_t min_age; TEH_plugin->rollback (TEH_plugin->cls); qs = TEH_plugin->get_purse_request ( TEH_plugin->cls, &rpc->pd.purse_pub, &merge_pub, &purse_expiration, &h_contract_terms, &min_age, &target_amount, &balance, &purse_sig); if (qs <= 0) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); GNUNET_break (0 != qs); TALER_LOG_WARNING ("Failed to fetch purse information from database\n"); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "select purse request"); return GNUNET_DB_STATUS_HARD_ERROR; } *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_CONFLICT, TALER_JSON_pack_ec ( TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA), TALER_JSON_pack_amount ("amount", &target_amount), GNUNET_JSON_pack_uint64 ("min_age", min_age), GNUNET_JSON_pack_timestamp ("purse_expiration", purse_expiration), GNUNET_JSON_pack_data_auto ("purse_sig", &purse_sig), GNUNET_JSON_pack_data_auto ("h_contract_terms", &h_contract_terms), GNUNET_JSON_pack_data_auto ("merge_pub", &merge_pub)); return GNUNET_DB_STATUS_HARD_ERROR; } } /* 2) create purse with reserve (and debit reserve for purse creation!) */ { bool in_conflict = true; bool insufficient_funds = true; bool no_reserve = true; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Creating purse with flags %d\n", rpc->flags); qs = TEH_plugin->do_reserve_purse ( TEH_plugin->cls, &rpc->pd.purse_pub, &rpc->merge_sig, rpc->merge_timestamp, &rpc->reserve_sig, (TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA == rpc->flags) ? NULL : &rpc->gf->fees.purse, rpc->reserve_pub, &in_conflict, &no_reserve, &insufficient_funds); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; TALER_LOG_WARNING ( "Failed to store purse merge information in database\n"); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "do reserve purse"); return GNUNET_DB_STATUS_HARD_ERROR; } if (in_conflict) { /* same purse already merged into a different reserve!? */ struct TALER_PurseContractPublicKeyP purse_pub; struct TALER_PurseMergeSignatureP merge_sig; struct GNUNET_TIME_Timestamp merge_timestamp; char *partner_url; struct TALER_ReservePublicKeyP reserve_pub; bool refunded; TEH_plugin->rollback (TEH_plugin->cls); qs = TEH_plugin->select_purse_merge ( TEH_plugin->cls, &purse_pub, &merge_sig, &merge_timestamp, &partner_url, &reserve_pub, &refunded); if (qs <= 0) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); GNUNET_break (0 != qs); TALER_LOG_WARNING ( "Failed to fetch purse merge information from database\n"); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "select purse merge"); return GNUNET_DB_STATUS_HARD_ERROR; } if (refunded) { /* This is a bit of a strange case ... */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Purse was already refunded\n"); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_GONE, TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, NULL); GNUNET_free (partner_url); return GNUNET_DB_STATUS_HARD_ERROR; } *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_CONFLICT, TALER_JSON_pack_ec ( TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA), GNUNET_JSON_pack_string ("partner_url", NULL == partner_url ? TEH_base_url : partner_url), GNUNET_JSON_pack_timestamp ("merge_timestamp", merge_timestamp), GNUNET_JSON_pack_data_auto ("merge_sig", &merge_sig), GNUNET_JSON_pack_data_auto ("reserve_pub", &reserve_pub)); GNUNET_free (partner_url); return GNUNET_DB_STATUS_HARD_ERROR; } if ( (no_reserve) && ( (TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA == rpc->flags) || (! TALER_amount_is_zero (&rpc->gf->fees.purse)) ) ) { *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_NOT_FOUND, TALER_JSON_pack_ec ( TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN)); return GNUNET_DB_STATUS_HARD_ERROR; } if (insufficient_funds) { *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_CONFLICT, TALER_JSON_pack_ec ( TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS)); return GNUNET_DB_STATUS_HARD_ERROR; } } /* 3) if present, persist contract */ if (! rpc->no_econtract) { bool in_conflict = true; qs = TEH_plugin->insert_contract (TEH_plugin->cls, &rpc->pd.purse_pub, &rpc->econtract, &in_conflict); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; TALER_LOG_WARNING ("Failed to store purse information in database\n"); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "purse purse contract"); return GNUNET_DB_STATUS_HARD_ERROR; } if (in_conflict) { struct TALER_EncryptedContract econtract; struct GNUNET_HashCode h_econtract; qs = TEH_plugin->select_contract_by_purse ( TEH_plugin->cls, &rpc->pd.purse_pub, &econtract); if (qs <= 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; GNUNET_break (0 != qs); TALER_LOG_WARNING ( "Failed to store fetch contract information from database\n"); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "select contract"); return GNUNET_DB_STATUS_HARD_ERROR; } GNUNET_CRYPTO_hash (econtract.econtract, econtract.econtract_size, &h_econtract); *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_CONFLICT, TALER_JSON_pack_ec ( TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA), GNUNET_JSON_pack_data_auto ("h_econtract", &h_econtract), GNUNET_JSON_pack_data_auto ("econtract_sig", &econtract.econtract_sig), GNUNET_JSON_pack_data_auto ("contract_pub", &econtract.contract_pub)); GNUNET_free (econtract.econtract); return GNUNET_DB_STATUS_HARD_ERROR; } } return qs; } MHD_RESULT TEH_handler_reserves_purse ( struct TEH_RequestContext *rc, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *root) { struct MHD_Connection *connection = rc->connection; struct ReservePurseContext rpc = { .reserve_pub = reserve_pub, .exchange_timestamp = GNUNET_TIME_timestamp_get () }; bool no_purse_fee = true; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("purse_value", TEH_currency, &rpc.pd.target_amount), GNUNET_JSON_spec_uint32 ("min_age", &rpc.min_age), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount ("purse_fee", TEH_currency, &rpc.purse_fee), &no_purse_fee), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_econtract ("econtract", &rpc.econtract), &rpc.no_econtract), GNUNET_JSON_spec_fixed_auto ("merge_pub", &rpc.merge_pub), GNUNET_JSON_spec_fixed_auto ("merge_sig", &rpc.merge_sig), GNUNET_JSON_spec_fixed_auto ("reserve_sig", &rpc.reserve_sig), GNUNET_JSON_spec_fixed_auto ("purse_pub", &rpc.pd.purse_pub), GNUNET_JSON_spec_fixed_auto ("purse_sig", &rpc.purse_sig), GNUNET_JSON_spec_fixed_auto ("h_contract_terms", &rpc.pd.h_contract_terms), GNUNET_JSON_spec_timestamp ("merge_timestamp", &rpc.merge_timestamp), GNUNET_JSON_spec_timestamp ("purse_expiration", &rpc.pd.purse_expiration), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, root, spec); if (GNUNET_SYSERR == res) { GNUNET_break (0); return MHD_NO; /* hard failure */ } if (GNUNET_NO == res) { GNUNET_break_op (0); return MHD_YES; /* failure */ } } { char *payto_uri; payto_uri = TALER_reserve_make_payto (TEH_base_url, reserve_pub); TALER_payto_hash (payto_uri, &rpc.h_payto); TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_purse_merge_verify (payto_uri, rpc.merge_timestamp, &rpc.pd.purse_pub, &rpc.merge_pub, &rpc.merge_sig)) { MHD_RESULT ret; GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID, payto_uri); GNUNET_free (payto_uri); return ret; } GNUNET_free (payto_uri); } GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &rpc.deposit_total)); if (GNUNET_TIME_timestamp_cmp (rpc.pd.purse_expiration, <, rpc.exchange_timestamp)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW, NULL); } if (GNUNET_TIME_absolute_is_never (rpc.pd.purse_expiration.abs_time)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER, NULL); } { struct TEH_KeyStateHandle *keys; keys = TEH_keys_get_state (); if (NULL == keys) { GNUNET_break (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); } rpc.gf = TEH_keys_global_fee_by_time (keys, rpc.exchange_timestamp); } if (NULL == rpc.gf) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Cannot purse purse: global fees not configured!\n"); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING, NULL); } if (no_purse_fee) { rpc.flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &rpc.purse_fee)); } else { rpc.flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; if (-1 == TALER_amount_cmp (&rpc.purse_fee, &rpc.gf->fees.purse)) { /* rpc.purse_fee is below gf.fees.purse! */ GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW, TALER_amount2s (&rpc.gf->fees.purse)); } } TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_purse_create_verify (rpc.pd.purse_expiration, &rpc.pd.h_contract_terms, &rpc.merge_pub, rpc.min_age, &rpc.pd.target_amount, &rpc.pd.purse_pub, &rpc.purse_sig)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID, NULL); } if (GNUNET_OK != TALER_wallet_account_merge_verify (rpc.merge_timestamp, &rpc.pd.purse_pub, rpc.pd.purse_expiration, &rpc.pd.h_contract_terms, &rpc.pd.target_amount, &rpc.purse_fee, rpc.min_age, rpc.flags, rpc.reserve_pub, &rpc.reserve_sig)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID, NULL); } if ( (! rpc.no_econtract) && (GNUNET_OK != TALER_wallet_econtract_upload_verify (rpc.econtract.econtract, rpc.econtract.econtract_size, &rpc.econtract.contract_pub, &rpc.pd.purse_pub, &rpc.econtract.econtract_sig)) ) { TALER_LOG_WARNING ("Invalid signature on /reserves/$PID/purse request\n"); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID, NULL); } if (GNUNET_SYSERR == TEH_plugin->preflight (TEH_plugin->cls)) { GNUNET_break (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, "preflight failure"); } /* execute transaction */ { MHD_RESULT mhd_ret; if (GNUNET_OK != TEH_DB_run_transaction (connection, "execute purse purse", TEH_MT_REQUEST_RESERVE_PURSE, &mhd_ret, &purse_transaction, &rpc)) { GNUNET_JSON_parse_free (spec); return mhd_ret; } } if (! rpc.kyc.ok) return TEH_RESPONSE_reply_kyc_required (connection, &rpc.h_payto, &rpc.kyc); /* generate regular response */ { MHD_RESULT res; res = TEH_RESPONSE_reply_purse_created (connection, rpc.exchange_timestamp, &rpc.deposit_total, &rpc.pd); GNUNET_JSON_parse_free (spec); return res; } } /* end of taler-exchange-httpd_reserves_purse.c */