exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 2cb7bc8f47e4455c2272fc22755b39c2f76adb6b
parent 43bb9eca02d40c97db776723f3fb3c1edbac8265
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 26 Mar 2026 17:03:41 +0100

rename backend files to be more consistent with endpoint names

Diffstat:
Msrc/exchange/Makefile.am | 116++++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/exchange/taler-exchange-httpd.c | 91++++++++++++++++++++++++++++++++++++++++---------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-accounts-get.c | 486-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-accounts-get.h | 44--------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-attributes-get.c | 814-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-attributes-get.h | 52----------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-decision.c | 535-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-decision.h | 67-------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-decisions-get.c | 205-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-legitimization-measures-get.c | 180-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-legitimization-measures-get.h | 43-------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-measures-get.c | 75---------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-measures-get.h | 45---------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-statistics-get.c | 173-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-statistics-get.h | 45---------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-transfer-get.c | 258-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_aml-transfer-get.h | 79-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_auditors.c | 232-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_auditors.h | 46----------------------------------------------
Dsrc/exchange/taler-exchange-httpd_batch-deposit.c | 1207-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_batch-deposit.h | 57---------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_blinding-prepare.c | 223-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_blinding-prepare.h | 40----------------------------------------
Dsrc/exchange/taler-exchange-httpd_coins_get.c | 763-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_coins_get.h | 53-----------------------------------------------------
Msrc/exchange/taler-exchange-httpd_common_deposit.c | 2+-
Dsrc/exchange/taler-exchange-httpd_config.c | 77-----------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_config.h | 58----------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_contract.c | 99-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_contract.h | 44--------------------------------------------
Msrc/exchange/taler-exchange-httpd_db.h | 2+-
Asrc/exchange/taler-exchange-httpd_delete-purses-PURSE_PUB.c | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_delete-purses-PURSE_PUB.h | 42++++++++++++++++++++++++++++++++++++++++++
Dsrc/exchange/taler-exchange-httpd_deposits_get.c | 536-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_deposits_get.h | 50--------------------------------------------------
Msrc/exchange/taler-exchange-httpd_extensions.c | 2+-
Asrc/exchange/taler-exchange-httpd_get-SPA.c | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-SPA.h | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-TERMS.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-TERMS.h | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c | 486+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.c | 815+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.c | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.c | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-measures.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-measures.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.c | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.h | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c | 763+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.h | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-config.c | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-config.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-contracts-CONTRACT_PUB.c | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-contracts-CONTRACT_PUB.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-deposits.c | 536+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-deposits.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-keys.c | 4428+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-keys.h | 614+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.c | 506+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.c | 825+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.c | 641+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-metrics.c | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-metrics.h | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-purses-PURSE_PUB-merge.c | 445+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-purses-PURSE_PUB-merge.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c | 633+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB.c | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.c | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-transfers-WTID.c | 641+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_get-transfers-WTID.h | 41+++++++++++++++++++++++++++++++++++++++++
Dsrc/exchange/taler-exchange-httpd_keys.c | 4428-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_keys.h | 614-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-check.c | 506-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-check.h | 49-------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-info.c | 825-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-info.h | 49-------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-proof.c | 641-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-proof.h | 49-------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-start.c | 537-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-start.h | 50--------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-upload.c | 567-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-upload.h | 53-----------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-wallet.c | 336-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_kyc-wallet.h | 52----------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_aml-officers.c | 142-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_auditors.c | 207-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_auditors_AP_disable.c | 196-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_denominations_HDP_revoke.c | 94-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_drain.c | 195-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_extensions.c | 300-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_global_fees.c | 261-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_partners.c | 132-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_post_keys.c | 574-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_signkey_EP_revoke.c | 93-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_wire_disable.c | 207-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_wire_enable.c | 346-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_management_wire_fees.c | 230-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_melt.c | 1864-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_melt.h | 52----------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_metrics.c | 160-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_metrics.h | 132-------------------------------------------------------------------------------
Asrc/exchange/taler-exchange-httpd_post-aml-OFFICER_PUB-decision.c | 535+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.c | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-batch-deposit.c | 1207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-batch-deposit.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-blinding-prepare.c | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-blinding-prepare.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-coins-COIN_PUB-refund.c | 369+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-coins-COIN_PUB-refund.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-kyc-start-ID.c | 537+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-kyc-start-ID.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-kyc-upload-ID.c | 567+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-kyc-upload-ID.h | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-kyc-wallet.c | 336+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-kyc-wallet.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-aml-officers.c | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-auditors-AUDITOR_PUB-disable.c | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-auditors.c | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-denominations-H_DENOM_PUB-revoke.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-drain.c | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-extensions.c | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-global-fees.c | 261+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-keys.c | 574+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-partners.c | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-signkeys-EXCHANGE_PUB-revoke.c | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-wire-disable.c | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-wire-fee.c | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-management-wire.c | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-melt.c | 1864+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-melt.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c | 664+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c | 509+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-merge.c | 815+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-merge.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-recoup-refresh.c | 431+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-recoup-refresh.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-recoup-withdraw.c | 443+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-recoup-withdraw.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c | 575+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c | 471+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.c | 894+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.c | 391+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reveal-melt.c | 877+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reveal-melt.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reveal-withdraw.c | 637+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-reveal-withdraw.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-withdraw.c | 1818+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_post-withdraw.h | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/exchange/taler-exchange-httpd_purses_create.c | 664-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_create.h | 47-----------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_delete.c | 149-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_delete.h | 42------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_deposit.c | 509-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_deposit.h | 47-----------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_get.c | 445-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_get.h | 51---------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_merge.c | 815-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_purses_merge.h | 54------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_recoup-refresh.c | 431-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_recoup-refresh.h | 46----------------------------------------------
Dsrc/exchange/taler-exchange-httpd_recoup.c | 443-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_recoup.h | 46----------------------------------------------
Dsrc/exchange/taler-exchange-httpd_refund.c | 369-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_refund.h | 47-----------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_attest.c | 391-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_attest.h | 41-----------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_close.c | 575-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_close.h | 48------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_get.c | 278-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_get.h | 54------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_get_attest.c | 195-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_get_attest.h | 44--------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_history.c | 633-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_history.h | 43-------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_open.c | 471-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_open.h | 41-----------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_purse.c | 894-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reserves_purse.h | 53-----------------------------------------------------
Msrc/exchange/taler-exchange-httpd_responses.c | 2+-
Dsrc/exchange/taler-exchange-httpd_reveal-melt.c | 877-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reveal-melt.h | 58----------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reveal-withdraw.c | 637-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_reveal-withdraw.h | 56--------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_spa.c | 128-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_spa.h | 63---------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_terms.c | 82-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_terms.h | 65-----------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_transfers_get.c | 641-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_transfers_get.h | 41-----------------------------------------
Dsrc/exchange/taler-exchange-httpd_withdraw.c | 1818-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_withdraw.h | 61-------------------------------------------------------------
Msrc/include/taler/taler_exchangedb_plugin.h | 2+-
211 files changed, 33130 insertions(+), 33128 deletions(-)

diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am @@ -145,72 +145,72 @@ taler_exchange_wirewatch_LDADD = \ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd.c taler-exchange-httpd.h \ - taler-exchange-httpd_aml-accounts-get.c taler-exchange-httpd_aml-accounts-get.h \ - taler-exchange-httpd_aml-attributes-get.c taler-exchange-httpd_aml-attributes-get.h \ - taler-exchange-httpd_aml-decision.c taler-exchange-httpd_aml-decision.h \ - taler-exchange-httpd_aml-decisions-get.c \ - taler-exchange-httpd_aml-legitimization-measures-get.c taler-exchange-httpd_aml-legitimization-measures-get.h \ - taler-exchange-httpd_aml-statistics-get.c taler-exchange-httpd_aml-statistics-get.h \ - taler-exchange-httpd_aml-measures-get.c taler-exchange-httpd_aml-measures-get.h \ - taler-exchange-httpd_aml-transfer-get.c taler-exchange-httpd_aml-transfer-get.h \ - taler-exchange-httpd_auditors.c taler-exchange-httpd_auditors.h \ - taler-exchange-httpd_batch-deposit.c taler-exchange-httpd_batch-deposit.h \ - taler-exchange-httpd_blinding-prepare.c taler-exchange-httpd_blinding-prepare.h \ - taler-exchange-httpd_coins_get.c taler-exchange-httpd_coins_get.h \ + taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.h \ + taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.c taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.h \ + taler-exchange-httpd_post-aml-OFFICER_PUB-decision.c taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h \ + taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c \ + taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.c taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.h \ + taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.c taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.h \ + taler-exchange-httpd_get-aml-OFFICER_PUB-measures.c taler-exchange-httpd_get-aml-OFFICER_PUB-measures.h \ + taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.c taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.h \ + taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.c taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.h \ + taler-exchange-httpd_post-batch-deposit.c taler-exchange-httpd_post-batch-deposit.h \ + taler-exchange-httpd_post-blinding-prepare.c taler-exchange-httpd_post-blinding-prepare.h \ + taler-exchange-httpd_get-coins-COIN_PUB-history.c taler-exchange-httpd_get-coins-COIN_PUB-history.h \ taler-exchange-httpd_common_deposit.c taler-exchange-httpd_common_deposit.h \ taler-exchange-httpd_common_kyc.c taler-exchange-httpd_common_kyc.h \ - taler-exchange-httpd_config.c taler-exchange-httpd_config.h \ - taler-exchange-httpd_contract.c taler-exchange-httpd_contract.h \ + taler-exchange-httpd_get-config.c taler-exchange-httpd_get-config.h \ + taler-exchange-httpd_get-contracts-CONTRACT_PUB.c taler-exchange-httpd_get-contracts-CONTRACT_PUB.h \ taler-exchange-httpd_db.c taler-exchange-httpd_db.h \ - taler-exchange-httpd_deposits_get.c taler-exchange-httpd_deposits_get.h \ + taler-exchange-httpd_get-deposits.c taler-exchange-httpd_get-deposits.h \ taler-exchange-httpd_extensions.c taler-exchange-httpd_extensions.h \ - taler-exchange-httpd_keys.c taler-exchange-httpd_keys.h \ - taler-exchange-httpd_kyc-check.c taler-exchange-httpd_kyc-check.h \ - taler-exchange-httpd_kyc-info.c taler-exchange-httpd_kyc-info.h \ - taler-exchange-httpd_kyc-proof.c taler-exchange-httpd_kyc-proof.h \ - taler-exchange-httpd_kyc-start.c taler-exchange-httpd_kyc-start.h \ - taler-exchange-httpd_kyc-upload.c taler-exchange-httpd_kyc-upload.h \ - taler-exchange-httpd_kyc-wallet.c taler-exchange-httpd_kyc-wallet.h \ + taler-exchange-httpd_get-keys.c taler-exchange-httpd_get-keys.h \ + taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.c taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.h \ + taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.c taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.h \ + taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.c taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.h \ + taler-exchange-httpd_post-kyc-start-ID.c taler-exchange-httpd_post-kyc-start-ID.h \ + taler-exchange-httpd_post-kyc-upload-ID.c taler-exchange-httpd_post-kyc-upload-ID.h \ + taler-exchange-httpd_post-kyc-wallet.c taler-exchange-httpd_post-kyc-wallet.h \ taler-exchange-httpd_kyc-webhook.c taler-exchange-httpd_kyc-webhook.h \ taler-exchange-httpd_management.h \ - taler-exchange-httpd_management_aml-officers.c \ - taler-exchange-httpd_management_auditors.c \ - taler-exchange-httpd_management_auditors_AP_disable.c \ - taler-exchange-httpd_management_denominations_HDP_revoke.c \ - taler-exchange-httpd_management_drain.c \ - taler-exchange-httpd_management_extensions.c \ - taler-exchange-httpd_management_global_fees.c \ - taler-exchange-httpd_management_partners.c \ - taler-exchange-httpd_management_post_keys.c \ - taler-exchange-httpd_management_signkey_EP_revoke.c \ - taler-exchange-httpd_management_wire_enable.c \ - taler-exchange-httpd_management_wire_disable.c \ - taler-exchange-httpd_management_wire_fees.c \ - taler-exchange-httpd_melt.c taler-exchange-httpd_melt.h \ - taler-exchange-httpd_metrics.c taler-exchange-httpd_metrics.h \ + taler-exchange-httpd_post-management-aml-officers.c \ + taler-exchange-httpd_post-management-auditors.c \ + taler-exchange-httpd_post-management-auditors-AUDITOR_PUB-disable.c \ + taler-exchange-httpd_post-management-denominations-H_DENOM_PUB-revoke.c \ + taler-exchange-httpd_post-management-drain.c \ + taler-exchange-httpd_post-management-extensions.c \ + taler-exchange-httpd_post-management-global-fees.c \ + taler-exchange-httpd_post-management-partners.c \ + taler-exchange-httpd_post-management-keys.c \ + taler-exchange-httpd_post-management-signkeys-EXCHANGE_PUB-revoke.c \ + taler-exchange-httpd_post-management-wire.c \ + taler-exchange-httpd_post-management-wire-disable.c \ + taler-exchange-httpd_post-management-wire-fee.c \ + taler-exchange-httpd_post-melt.c taler-exchange-httpd_post-melt.h \ + taler-exchange-httpd_get-metrics.c taler-exchange-httpd_get-metrics.h \ taler-exchange-httpd_mhd.c taler-exchange-httpd_mhd.h \ - taler-exchange-httpd_purses_create.c taler-exchange-httpd_purses_create.h \ - taler-exchange-httpd_purses_deposit.c taler-exchange-httpd_purses_deposit.h \ - taler-exchange-httpd_purses_delete.c taler-exchange-httpd_purses_delete.h \ - taler-exchange-httpd_purses_get.c taler-exchange-httpd_purses_get.h \ - taler-exchange-httpd_purses_merge.c taler-exchange-httpd_purses_merge.h \ - taler-exchange-httpd_recoup.c taler-exchange-httpd_recoup.h \ - taler-exchange-httpd_recoup-refresh.c taler-exchange-httpd_recoup-refresh.h \ - taler-exchange-httpd_refund.c taler-exchange-httpd_refund.h \ - taler-exchange-httpd_reserves_attest.c taler-exchange-httpd_reserves_attest.h \ - taler-exchange-httpd_reserves_close.c taler-exchange-httpd_reserves_close.h \ - taler-exchange-httpd_reserves_get.c taler-exchange-httpd_reserves_get.h \ - taler-exchange-httpd_reserves_get_attest.c taler-exchange-httpd_reserves_get_attest.h \ - taler-exchange-httpd_reserves_history.c taler-exchange-httpd_reserves_history.h \ - taler-exchange-httpd_reserves_open.c taler-exchange-httpd_reserves_open.h \ - taler-exchange-httpd_reserves_purse.c taler-exchange-httpd_reserves_purse.h \ + taler-exchange-httpd_post-purses-PURSE_PUB-create.c taler-exchange-httpd_post-purses-PURSE_PUB-create.h \ + taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h \ + taler-exchange-httpd_delete-purses-PURSE_PUB.c taler-exchange-httpd_delete-purses-PURSE_PUB.h \ + taler-exchange-httpd_get-purses-PURSE_PUB-merge.c taler-exchange-httpd_get-purses-PURSE_PUB-merge.h \ + taler-exchange-httpd_post-purses-PURSE_PUB-merge.c taler-exchange-httpd_post-purses-PURSE_PUB-merge.h \ + taler-exchange-httpd_post-recoup-withdraw.c taler-exchange-httpd_post-recoup-withdraw.h \ + taler-exchange-httpd_post-recoup-refresh.c taler-exchange-httpd_post-recoup-refresh.h \ + taler-exchange-httpd_post-coins-COIN_PUB-refund.c taler-exchange-httpd_post-coins-COIN_PUB-refund.h \ + taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.c taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.h \ + taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c taler-exchange-httpd_post-reserves-RESERVE_PUB-close.h \ + taler-exchange-httpd_get-reserves-RESERVE_PUB.c taler-exchange-httpd_get-reserves-RESERVE_PUB.h \ + taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.c taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.h \ + taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c taler-exchange-httpd_get-reserves-RESERVE_PUB-history.h \ + taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c taler-exchange-httpd_post-reserves-RESERVE_PUB-open.h \ + taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.c taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h \ taler-exchange-httpd_responses.c taler-exchange-httpd_responses.h \ - taler-exchange-httpd_reveal-melt.c taler-exchange-httpd_reveal-melt.h \ - taler-exchange-httpd_reveal-withdraw.c taler-exchange-httpd_reveal-withdraw.h \ - taler-exchange-httpd_spa.c taler-exchange-httpd_spa.h \ - taler-exchange-httpd_terms.c taler-exchange-httpd_terms.h \ - taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h \ - taler-exchange-httpd_withdraw.c taler-exchange-httpd_withdraw.h + taler-exchange-httpd_post-reveal-melt.c taler-exchange-httpd_post-reveal-melt.h \ + taler-exchange-httpd_post-reveal-withdraw.c taler-exchange-httpd_post-reveal-withdraw.h \ + taler-exchange-httpd_get-SPA.c taler-exchange-httpd_get-SPA.h \ + taler-exchange-httpd_get-TERMS.c taler-exchange-httpd_get-TERMS.h \ + taler-exchange-httpd_get-transfers-WTID.c taler-exchange-httpd_get-transfers-WTID.h \ + taler-exchange-httpd_post-withdraw.c taler-exchange-httpd_post-withdraw.h taler_exchange_httpd_LDADD = \ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c @@ -31,55 +31,56 @@ #include "taler/taler_kyclogic_lib.h" #include "taler/taler_templating_lib.h" #include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_withdraw.h" -#include "taler-exchange-httpd_aml-accounts-get.h" -#include "taler-exchange-httpd_aml-attributes-get.h" -#include "taler-exchange-httpd_aml-decision.h" -#include "taler-exchange-httpd_aml-legitimization-measures-get.h" -#include "taler-exchange-httpd_aml-statistics-get.h" -#include "taler-exchange-httpd_aml-transfer-get.h" -#include "taler-exchange-httpd_aml-measures-get.h" -#include "taler-exchange-httpd_auditors.h" -#include "taler-exchange-httpd_batch-deposit.h" -#include "taler-exchange-httpd_blinding-prepare.h" -#include "taler-exchange-httpd_coins_get.h" -#include "taler-exchange-httpd_config.h" -#include "taler-exchange-httpd_contract.h" -#include "taler-exchange-httpd_deposits_get.h" +#include "taler-exchange-httpd_post-withdraw.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.h" +#include \ + "taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.h" +#include "taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-measures.h" +#include "taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.h" +#include "taler-exchange-httpd_post-batch-deposit.h" +#include "taler-exchange-httpd_post-blinding-prepare.h" +#include "taler-exchange-httpd_get-coins-COIN_PUB-history.h" +#include "taler-exchange-httpd_get-config.h" +#include "taler-exchange-httpd_get-contracts-CONTRACT_PUB.h" +#include "taler-exchange-httpd_get-deposits.h" #include "taler-exchange-httpd_extensions.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_kyc-check.h" -#include "taler-exchange-httpd_kyc-info.h" -#include "taler-exchange-httpd_kyc-proof.h" -#include "taler-exchange-httpd_kyc-start.h" -#include "taler-exchange-httpd_kyc-upload.h" -#include "taler-exchange-httpd_kyc-wallet.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.h" +#include "taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.h" +#include "taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.h" +#include "taler-exchange-httpd_post-kyc-start-ID.h" +#include "taler-exchange-httpd_post-kyc-upload-ID.h" +#include "taler-exchange-httpd_post-kyc-wallet.h" #include "taler-exchange-httpd_kyc-webhook.h" -#include "taler-exchange-httpd_aml-decision.h" +#include "taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h" #include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_melt.h" -#include "taler-exchange-httpd_metrics.h" +#include "taler-exchange-httpd_post-melt.h" +#include "taler-exchange-httpd_get-metrics.h" #include "taler-exchange-httpd_mhd.h" -#include "taler-exchange-httpd_purses_create.h" -#include "taler-exchange-httpd_purses_deposit.h" -#include "taler-exchange-httpd_purses_get.h" -#include "taler-exchange-httpd_purses_delete.h" -#include "taler-exchange-httpd_purses_merge.h" -#include "taler-exchange-httpd_recoup.h" -#include "taler-exchange-httpd_recoup-refresh.h" -#include "taler-exchange-httpd_refund.h" -#include "taler-exchange-httpd_reserves_attest.h" -#include "taler-exchange-httpd_reserves_close.h" -#include "taler-exchange-httpd_reserves_get.h" -#include "taler-exchange-httpd_reserves_get_attest.h" -#include "taler-exchange-httpd_reserves_history.h" -#include "taler-exchange-httpd_reserves_open.h" -#include "taler-exchange-httpd_reserves_purse.h" -#include "taler-exchange-httpd_reveal-withdraw.h" -#include "taler-exchange-httpd_reveal-melt.h" -#include "taler-exchange-httpd_spa.h" -#include "taler-exchange-httpd_terms.h" -#include "taler-exchange-httpd_transfers_get.h" +#include "taler-exchange-httpd_post-purses-PURSE_PUB-create.h" +#include "taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h" +#include "taler-exchange-httpd_get-purses-PURSE_PUB-merge.h" +#include "taler-exchange-httpd_delete-purses-PURSE_PUB.h" +#include "taler-exchange-httpd_post-purses-PURSE_PUB-merge.h" +#include "taler-exchange-httpd_post-recoup-withdraw.h" +#include "taler-exchange-httpd_post-recoup-refresh.h" +#include "taler-exchange-httpd_post-coins-COIN_PUB-refund.h" +#include "taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.h" +#include "taler-exchange-httpd_post-reserves-RESERVE_PUB-close.h" +#include "taler-exchange-httpd_get-reserves-RESERVE_PUB.h" +#include "taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.h" +#include "taler-exchange-httpd_get-reserves-RESERVE_PUB-history.h" +#include "taler-exchange-httpd_post-reserves-RESERVE_PUB-open.h" +#include "taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h" +#include "taler-exchange-httpd_post-reveal-withdraw.h" +#include "taler-exchange-httpd_post-reveal-melt.h" +#include "taler-exchange-httpd_get-SPA.h" +#include "taler-exchange-httpd_get-TERMS.h" +#include "taler-exchange-httpd_get-transfers-WTID.h" #include "taler/taler_exchangedb_lib.h" #include "taler/taler_exchangedb_plugin.h" #include "taler/taler_extensions.h" diff --git a/src/exchange/taler-exchange-httpd_aml-accounts-get.c b/src/exchange/taler-exchange-httpd_aml-accounts-get.c @@ -1,486 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024, 2025, 2026 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-accounts-get.c - * @brief Return summary information about accounts - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler-exchange-httpd_aml-accounts-get.h" - - -/** - * Maximum number of records we return in one go. Must be - * small enough to ensure that XML/CSV encoded results stay - * below the 16MB limit of GNUNET_malloc(). - */ -#define MAX_RECORDS (1024 * 64) - -#define CSV_HEADER \ - "File number,Customer,Comments,Risky,Acquisition date,Exit date\r\n" -#define CSV_FOOTER "\r\n" - -#define XML_HEADER "<?xml version=\"1.0\" encoding=\"UTF 8\"?>" \ - "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \ - " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \ - " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \ - " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \ - " xmlns:o=\"urn:schemas-microsoft-com:office:office\"" \ - " xmlns:x=\"urn:schemas-microsoft-com:office:excel\"" \ - " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \ - "<Styles><Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd\"/></Style></Styles>\n" \ - "<Worksheet ss:Name=\"Accounts\">\n" \ - "<Table>\n" \ - "<Row>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">File number</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Customer</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Comments</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Increased risk business relationship</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Acquisition date</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Exit date</Data></Cell>\n" \ - "</Row>\n" -#define XML_FOOTER "</Table></Worksheet></Workbook>" - -/** - * Closure for the record_cb(). - */ -struct ResponseContext -{ - /** - * Format of the response we are to generate. - */ - enum - { - RCF_JSON, - RCF_XML, - RCF_CSV - } format; - - /** - * Where we store the response data. - */ - union - { - /** - * If @e format is #RCF_JSON. - */ - json_t *json; - - /** - * If @e format is #RCF_XML. - */ - struct GNUNET_Buffer xml; - - /** - * If @e format is #RCF_CSV. - */ - struct GNUNET_Buffer csv; - - } details; -}; - - -/** - * Free resources from @a rc - * - * @param[in] rc context to clean up - */ -static void -free_rc (struct ResponseContext *rc) -{ - switch (rc->format) - { - case RCF_JSON: - json_decref (rc->details.json); - break; - case RCF_XML: - GNUNET_buffer_clear (&rc->details.xml); - break; - case RCF_CSV: - GNUNET_buffer_clear (&rc->details.csv); - break; - } -} - - -/** - * Return account summary information. - * - * @param cls closure - * @param row_id current row in AML status table - * @param h_payto account for which the attribute data is stored - * @param open_time when was the account opened formally, - * GNUNET_TIME_UNIT_FOREVER_TS if it was never opened - * @param close_time when was the account formally closed, - * GNUNET_TIME_UNIT_ZERO_TS if it was never closed - * @param comments comments on the account - * @param high_risk is this a high-risk business relationship - * @param to_investigate TRUE if this account should be investigated - * @param payto the payto URI of the account - */ -static void -record_cb ( - void *cls, - uint64_t row_id, - const struct TALER_NormalizedPaytoHashP *h_payto, - struct GNUNET_TIME_Timestamp open_time, - struct GNUNET_TIME_Timestamp close_time, - const char *comments, - bool high_risk, - bool to_investigate, - struct TALER_FullPayto payto) -{ - struct ResponseContext *rc = cls; - - if ( (NULL == comments) && - (GNUNET_TIME_absolute_is_never (open_time.abs_time)) ) - comments = "transacted amounts below limits that trigger account opening"; - if (NULL == comments) - comments = ""; - switch (rc->format) - { - case RCF_JSON: - GNUNET_assert ( - 0 == - json_array_append_new ( - rc->details.json, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("h_payto", - h_payto), - TALER_JSON_pack_full_payto ("full_payto", - payto), - GNUNET_JSON_pack_bool ("high_risk", - high_risk), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("comments", - comments)), - GNUNET_JSON_pack_int64 ("rowid", - row_id), - GNUNET_JSON_pack_timestamp ("open_time", - open_time), - GNUNET_JSON_pack_timestamp ("close_time", - close_time), - GNUNET_JSON_pack_bool ("to_investigate", - to_investigate) - ))); - return; - case RCF_XML: - { - char *epayto; - char *ecomments = NULL; - char opentime_s[128]; - char closetime_s[128]; - const struct tm *tm; - time_t tt; - - epayto = TALER_escape_xml (payto.full_payto); - if ( (NULL == comments) && - (GNUNET_TIME_absolute_is_never (open_time.abs_time)) ) - comments = - "transacted amounts below limits that trigger account opening"; - ecomments = TALER_escape_xml (comments); - tt = (time_t) GNUNET_TIME_timestamp_to_s (open_time); - tm = gmtime (&tt); - strftime (opentime_s, - sizeof (opentime_s), - "%Y-%m-%dT%H:%M:%S", - tm); - tt = (time_t) GNUNET_TIME_timestamp_to_s (close_time); - tm = gmtime (&tt); - strftime (closetime_s, - sizeof (closetime_s), - "%Y-%m-%dT%H:%M:%S", - tm); - GNUNET_buffer_write_fstr ( - &rc->details.xml, - "<Row>" - "<Cell><Data ss:Type=\"Number\">%llu</Data></Cell>" - "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>" - "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>" - "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>" - "</Row>\n", - (unsigned long long) row_id, - epayto, - NULL == ecomments - ? "" - : ecomments, - high_risk ? "TRUE" : "FALSE", - high_risk ? "1" : "0", - GNUNET_TIME_absolute_is_never (open_time.abs_time) - ? "String" - : "DateTime", - GNUNET_TIME_absolute_is_never (open_time.abs_time) - ? "never" - : opentime_s, - GNUNET_TIME_absolute_is_never (close_time.abs_time) - ? "String" - : "DateTime", - GNUNET_TIME_absolute_is_never (close_time.abs_time) - ? "never" - : closetime_s); - GNUNET_free (ecomments); - GNUNET_free (epayto); - break; - } /* end case RCF_XML */ - case RCF_CSV: - { - char *ecomments; - char otbuf[64]; - char ctbuf[64]; - size_t len = strlen (comments); - size_t wpos = 0; - - GNUNET_snprintf (otbuf, - sizeof (otbuf), - "%s", - GNUNET_TIME_timestamp2s (open_time)); - GNUNET_snprintf (ctbuf, - sizeof (ctbuf), - "%s", - GNUNET_TIME_timestamp2s (close_time)); - /* Escape 'comments' to double '"' as per RFC 4180, 2.7. */ - ecomments = GNUNET_malloc (2 * len + 1); - for (size_t off = 0; off<len; off++) - { - if ('"' == comments[off]) - ecomments[wpos++] = '"'; - ecomments[wpos++] = comments[off]; - } - GNUNET_buffer_write_fstr (&rc->details.csv, - "%llu,\"%s\",\"%s\",%s,%s,%s\r\n", - (unsigned long long) row_id, - payto.full_payto, - ecomments, - high_risk ? "X":" ", - GNUNET_TIME_absolute_is_never (open_time. - abs_time) - ? "-" - : otbuf, - GNUNET_TIME_absolute_is_never (close_time. - abs_time) - ? "-" - : ctbuf); - GNUNET_free (ecomments); - break; - } /* end case RCF_CSV */ - } /* end switch */ -} - - -MHD_RESULT -TEH_handler_aml_accounts_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - struct ResponseContext rctx; - int64_t limit = -20; - uint64_t offset; - enum TALER_EXCHANGE_YesNoAll open_filter; - enum TALER_EXCHANGE_YesNoAll high_risk_filter; - enum TALER_EXCHANGE_YesNoAll investigation_filter; - - memset (&rctx, - 0, - sizeof (rctx)); - if (NULL != args[0]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - args[0]); - } - TALER_MHD_parse_request_snumber (rc->connection, - "limit", - &limit); - if (limit > 0) - offset = 0; - else - offset = INT64_MAX; - TALER_MHD_parse_request_number (rc->connection, - "offset", - &offset); - if (offset > INT64_MAX) - { - GNUNET_break_op (0); /* broken client */ - offset = INT64_MAX; - } - TALER_MHD_parse_request_yna (rc->connection, - "open", - TALER_EXCHANGE_YNA_ALL, - &open_filter); - TALER_MHD_parse_request_yna (rc->connection, - "investigation", - TALER_EXCHANGE_YNA_ALL, - &investigation_filter); - TALER_MHD_parse_request_yna (rc->connection, - "high_risk", - TALER_EXCHANGE_YNA_ALL, - &high_risk_filter); - { - const char *mime; - - mime = MHD_lookup_connection_value (rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT); - if (NULL == mime) - mime = "application/json"; - if (0 == strcmp (mime, - "application/json")) - { - rctx.format = RCF_JSON; - rctx.details.json = json_array (); - GNUNET_assert (NULL != rctx.details.json); - } - else if (0 == strcmp (mime, - "application/vnd.ms-excel")) - { - rctx.format = RCF_XML; - GNUNET_buffer_write_str (&rctx.details.xml, - XML_HEADER); - } - else if (0 == strcmp (mime, - "text/csv")) - { - rctx.format = RCF_CSV; - GNUNET_buffer_write_str (&rctx.details.csv, - CSV_HEADER); - } - else - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_ACCEPTABLE, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - mime); - } - } - - { - enum GNUNET_DB_QueryStatus qs; - - if (limit > MAX_RECORDS) - limit = MAX_RECORDS; - if (limit < -MAX_RECORDS) - limit = -MAX_RECORDS; - qs = TEH_plugin->select_kyc_accounts ( - TEH_plugin->cls, - investigation_filter, - open_filter, - high_risk_filter, - offset, - limit, - &record_cb, - &rctx); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - free_rc (&rctx); - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_kyx_accounts"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - free_rc (&rctx); - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } /* end switch (qs) */ - - switch (rctx.format) - { - case RCF_JSON: - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("accounts", - rctx.details.json)); - case RCF_XML: - { - struct MHD_Response *resp; - MHD_RESULT mret; - - GNUNET_buffer_write_str (&rctx.details.xml, - XML_FOOTER); - /* FIXME: add support for compression */ - resp = MHD_create_response_from_buffer (rctx.details.xml.position, - rctx.details.xml.mem, - MHD_RESPMEM_MUST_FREE); - TALER_MHD_add_global_headers (resp, - false); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_CONTENT_TYPE, - "application/vnd.ms-excel")); - mret = MHD_queue_response (rc->connection, - MHD_HTTP_OK, - resp); - MHD_destroy_response (resp); - return mret; - } - case RCF_CSV: - { - struct MHD_Response *resp; - MHD_RESULT mret; - - GNUNET_buffer_write_str (&rctx.details.csv, - CSV_FOOTER); - /* FIXME: add support for compression */ - resp = MHD_create_response_from_buffer (rctx.details.csv.position, - rctx.details.csv.mem, - MHD_RESPMEM_MUST_FREE); - TALER_MHD_add_global_headers (resp, - false); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_CONTENT_TYPE, - "text/csv")); - mret = MHD_queue_response (rc->connection, - MHD_HTTP_OK, - resp); - MHD_destroy_response (resp); - return mret; - } - } /* end switch (rctx.format) */ - } - GNUNET_break (0); - return MHD_NO; -} - - -/* end of taler-exchange-httpd_aml-accounts_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-accounts-get.h b/src/exchange/taler-exchange-httpd_aml-accounts-get.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-accounts-get.h - * @brief Handle GET /aml/$OFFICER_PUB/accounts requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AML_ACCOUNTS_GET_H -#define TALER_EXCHANGE_HTTPD_AML_ACCOUNTS_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/aml/$OFFICER_PUB/accounts" request. Parses the request - * details, checks the signatures and if appropriately authorized returns - * the matching decisions. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be the state) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_accounts_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_aml-attributes-get.c b/src/exchange/taler-exchange-httpd_aml-attributes-get.c @@ -1,814 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024, 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-attributes-get.c - * @brief Return summary information about KYC attributes - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler-exchange-httpd_aml-attributes-get.h" -#include "taler-exchange-httpd_metrics.h" - -/** - * Maximum number of records we return in one request. - */ -#define MAX_RECORDS 1024 - - -/** - * Closure for the detail_cb(). - */ -struct ResponseContext -{ - /** - * Format of the response we are to generate. - */ - enum - { - RCF_JSON, - RCF_PDF - } format; - - /** - * Stored in a DLL while suspended. - */ - struct ResponseContext *next; - - /** - * Stored in a DLL while suspended. - */ - struct ResponseContext *prev; - - /** - * Context for this request. - */ - struct TEH_RequestContext *rc; - - /** - * Async context used to run Typst. - */ - struct TALER_MHD_TypstContext *tc; - - /** - * Response to return. - */ - struct MHD_Response *response; - - /** - * HTTP status to use with @e response. - */ - unsigned int http_status; - - /** - * True if this is for a wallet. - */ - bool is_wallet; - - /** - * Where we store the response data. - */ - union - { - /** - * If @e format is #RCF_JSON. - */ - json_t *json; - - /** - * If @e format is #RCF_PDF. - */ - struct - { - - /** - * Typst forms to compile. - */ - struct TALER_MHD_TypstDocument docs[MAX_RECORDS]; - - /** - * JSON data for each Typst form. - */ - json_t *jdata[MAX_RECORDS]; - - /** - * Next write offset into @e docs and @e jdata. - */ - size_t off; - - /** - * Global attributes we need to inject in to each @e jdata. - */ - json_t *global_attrs; - - } pdf; - - } details; -}; - - -/** - * DLL of requests awaiting Typst. - */ -static struct ResponseContext *rctx_head; - -/** - * DLL of requests awaiting Typst. - */ -static struct ResponseContext *rctx_tail; - - -void -TEH_handler_aml_attributes_get_cleanup () -{ - struct ResponseContext *rctx; - - while (NULL != (rctx = rctx_head)) - { - GNUNET_CONTAINER_DLL_remove (rctx_head, - rctx_tail, - rctx); - MHD_resume_connection (rctx->rc->connection); - } -} - - -/** - * Free resources from @a rc - * - * @param[in] rc context to clean up - */ -static void -free_rc (struct TEH_RequestContext *rc) -{ - struct ResponseContext *rctx = rc->rh_ctx; - - if (NULL != rctx->tc) - { - TALER_MHD_typst_cancel (rctx->tc); - rctx->tc = NULL; - } - if (NULL != rctx->response) - { - MHD_destroy_response (rctx->response); - rctx->response = NULL; - } - switch (rctx->format) - { - case RCF_JSON: - json_decref (rctx->details.json); - break; - case RCF_PDF: - for (size_t i = 0; i<rctx->details.pdf.off; i++) - { - json_decref (rctx->details.pdf.jdata[i]); - rctx->details.pdf.jdata[i] = NULL; - } - json_decref (rctx->details.pdf.global_attrs); - rctx->details.pdf.global_attrs = NULL; - break; - } - GNUNET_free (rctx); -} - - -/** - * Dump attachment @a attach into @a rc. - * - * @param[in,out] rc response context to update - * @param attach attachment to dump - * @return true on success - */ -static bool -dump_attachment (struct ResponseContext *rc, - const json_t *attach) -{ - if (! json_is_string (attach)) - return false; - if (rc->details.pdf.off >= MAX_RECORDS) - { - GNUNET_break (0); - return false; - } - rc->details.pdf.jdata[rc->details.pdf.off] - = json_incref ((json_t *) attach); - rc->details.pdf.docs[rc->details.pdf.off].form_name - = NULL; /* inline PDF */ - rc->details.pdf.docs[rc->details.pdf.off].form_version - = NULL; /* none */ - rc->details.pdf.docs[rc->details.pdf.off].data - = rc->details.pdf.jdata[rc->details.pdf.off]; - rc->details.pdf.off++; - return true; -} - - -/** - * Recursively scan @a attrs for ".FILE" members and dump those - * attachments into @a rc. - * - * @param[in,out] rc response context to update - * @param attrs attributes to scan recursively - * @return true on success - */ -static bool -dump_attachments (struct ResponseContext *rc, - const json_t *attrs) -{ - bool ret = true; - - if (json_is_array (attrs)) - { - const json_t *e; - size_t i; - - json_array_foreach ((json_t *) attrs, i, e) - if (! dump_attachments (rc, - e)) - ret = false; - } - if (json_is_object (attrs)) - { - const json_t *e; - const char *k; - - json_object_foreach ((json_t *) attrs, k, e) - { - if (0 == strcmp (k, - "CONTENTS")) - { - const char *v; - - /* Make sure this is the supported encoding */ - v = json_string_value (json_object_get (attrs, - "ENCODING")); - if (NULL == v) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "`CONTENTS' attribute found without `ENCODING', skipping dumping attachment\n"); - continue; /* maybe not a file!? */ - } - if (0 != strcmp (v, - "base64")) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Attachment with unsupported encoding `%s' encountered, skipping attachment in PDF generation\n", - v); - continue; - } - v = json_string_value (json_object_get (attrs, - "MIME_TYPE")); - if (NULL == v) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "`CONTENTS' attribute found without `MIME_TYPE', skipping dumping attachment\n"); - continue; - } - if (0 != strcmp (v, - "application/pdf")) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Attachment with unsupported mime type `%s' encountered, skipping attachment in PDF generation\n", - v); - continue; - } - /* Filename is optional, only log it */ - v = json_string_value (json_object_get (attrs, - "FILENAME")); - if (NULL != v) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Dumping attachment `%s'\n", - v); - } - if (! dump_attachment (rc, - e)) - ret = false; - } - else if (! dump_attachments (rc, - e)) - ret = false; - } - } - return ret; -} - - -/** - * Return AML account attributes. - * - * @param cls closure - * @param row_id current row in kyc_attributes table - * @param collection_time when were the attributes collected - * @param by_aml_officer was the attribute set filed by the AML officer - * @param staff_name name of the officer, NULL if not @a by_aml_officer - * @param enc_attributes_size length of @a enc_attributes - * @param enc_attributes the encrypted collected attributes - */ -static void -detail_cb ( - void *cls, - uint64_t row_id, - struct GNUNET_TIME_Timestamp collection_time, - bool by_aml_officer, - const char *staff_name, - size_t enc_attributes_size, - const void *enc_attributes) -{ - static char *datadir = NULL; - struct ResponseContext *rc = cls; - json_t *attrs; - const char *form_id; - - if (NULL == datadir) - { - datadir = GNUNET_OS_installation_get_path ( - TALER_EXCHANGE_project_data (), - GNUNET_OS_IPK_DATADIR); - } - attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key, - enc_attributes, - enc_attributes_size); - if (NULL == attrs) - { - GNUNET_break (0); - return; - } - form_id = json_string_value (json_object_get (attrs, - "FORM_ID")); - if (NULL == form_id) - { - GNUNET_break (0); - return; - } - switch (rc->format) - { - case RCF_JSON: - GNUNET_assert ( - 0 == - json_array_append_new ( - rc->details.json, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_int64 ("rowid", - row_id), - GNUNET_JSON_pack_bool ("by_aml_officer", - by_aml_officer), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("attributes", - attrs)), - GNUNET_JSON_pack_timestamp ("collection_time", - collection_time) - ))); - break; - case RCF_PDF: - if (rc->details.pdf.off >= MAX_RECORDS) - { - GNUNET_break (0); - return; - } - GNUNET_assert (0 == - json_object_set_new (attrs, - "DATADIR", - json_string (datadir))); - GNUNET_assert (0 == - json_object_set_new (attrs, - "BY_AML_OFFICER", - json_boolean (by_aml_officer))); - GNUNET_assert (0 == - json_object_set_new (attrs, - "FILING_DATE", - json_string ( - GNUNET_STRINGS_timestamp_to_string ( - collection_time)))); - if (by_aml_officer) - GNUNET_assert (0 == - json_object_set_new (attrs, - "AML_STAFF_NAME", - json_string (staff_name))); - { - char *have; - - GNUNET_asprintf (&have, - "HAVE_%s", - form_id); - GNUNET_assert (0 == - json_object_set_new (rc->details.pdf.global_attrs, - have, - json_true ())); - GNUNET_free (have); - } - rc->details.pdf.jdata[rc->details.pdf.off] - = attrs; - rc->details.pdf.docs[rc->details.pdf.off].form_name - = form_id; - rc->details.pdf.docs[rc->details.pdf.off].form_version - = NULL; /* not yet supported! */ - rc->details.pdf.docs[rc->details.pdf.off].data - = rc->details.pdf.jdata[rc->details.pdf.off]; - rc->details.pdf.off++; - if (! dump_attachments (rc, - attrs)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to dump some attachment!\n"); - } - break; - } -} - - -/** - * Function called with the result of a #TALER_MHD_typst() operation. - * - * @param cls closure - * @param tr result of the operation - */ -static void -pdf_cb (void *cls, - const struct TALER_MHD_TypstResponse *tr) -{ - struct ResponseContext *rctx = cls; - - rctx->tc = NULL; - GNUNET_CONTAINER_DLL_remove (rctx_head, - rctx_tail, - rctx); - MHD_resume_connection (rctx->rc->connection); - TALER_MHD_daemon_trigger (); - if (TALER_EC_NONE != tr->ec) - { - rctx->http_status - = TALER_ErrorCode_get_http_status (tr->ec); - rctx->response - = TALER_MHD_make_error (tr->ec, - tr->details.hint); - return; - } - rctx->http_status - = MHD_HTTP_OK; - rctx->response - = TALER_MHD_response_from_pdf_file (tr->details.filename); -} - - -/** - * Function called with the latest AML decision on an - * account. Used to build the cover page. - * - * @param cls a `struct ResponseContext *` - * @param outcome_serial_id row ID of the decision - * @param decision_time when was the decision taken - * @param justification what was the given justification - * @param decider_pub which key signed the decision - * @param jproperties what are the new account properties - * @param jnew_rules what are the new account rules - * @param to_investigate should AML staff investigate - * after the decision - * @param is_active is this the active decision - */ -static void -build_cover_page ( - void *cls, - uint64_t outcome_serial_id, - struct GNUNET_TIME_Timestamp decision_time, - const char *justification, - const struct TALER_AmlOfficerPublicKeyP *decider_pub, - const json_t *jproperties, - const json_t *jnew_rules, - bool to_investigate, - bool is_active) -{ - struct ResponseContext *rc = cls; - json_t *cover; - json_t *defr = NULL; - - if (NULL == jnew_rules) - { - defr = TALER_KYCLOGIC_get_default_legi_rules (rc->is_wallet); - jnew_rules = defr; - } - cover = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("properties", - (json_t *) jproperties)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("rules", - (json_t *) jnew_rules)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("last_justification", - justification)), - GNUNET_JSON_pack_bool ("is_active", - is_active), - GNUNET_JSON_pack_bool ("to_investigate", - to_investigate)); - if (NULL != defr) - json_decref (defr); - /* first page, so we really cannot have hit the maximum yet */ - GNUNET_assert (rc->details.pdf.off < MAX_RECORDS); - rc->details.pdf.jdata[rc->details.pdf.off] - = cover; - rc->details.pdf.docs[rc->details.pdf.off].form_name - = "_cover_"; - rc->details.pdf.docs[rc->details.pdf.off].form_version - = NULL; /* not yet supported! */ - rc->details.pdf.docs[rc->details.pdf.off].data - = rc->details.pdf.jdata[rc->details.pdf.off]; - rc->details.pdf.off++; -} - - -MHD_RESULT -TEH_handler_aml_attributes_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - struct ResponseContext *rctx = rc->rh_ctx; - int64_t limit = -20; - uint64_t offset; - struct TALER_NormalizedPaytoHashP h_payto; - uint64_t file_number; - - if (NULL == rctx) - { - rctx = GNUNET_new (struct ResponseContext); - rctx->rc = rc; - rc->rh_ctx = rctx; - rc->rh_cleaner = &free_rc; - } - else - { - if (NULL == rctx->response) - { - GNUNET_break (0); - return MHD_NO; - } - return MHD_queue_response (rc->connection, - rctx->http_status, - rctx->response); - } - if ( (NULL == args[0]) || - (NULL != args[1]) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - rc->url); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &h_payto, - sizeof (h_payto))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PATH_SEGMENT_MALFORMED, - "h_payto"); - } - - TALER_MHD_parse_request_snumber (rc->connection, - "limit", - &limit); - if (limit > 0) - offset = 0; - else - offset = INT64_MAX; - TALER_MHD_parse_request_number (rc->connection, - "offset", - &offset); - - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->lookup_aml_file_number (TEH_plugin->cls, - &h_payto, - &file_number, - &rctx->is_wallet); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_aml_file_number"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Account unknown, return 404 */ - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_TARGET_ACCOUNT_UNKNOWN, - args[0]); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - - { - const char *mime; - - mime = MHD_lookup_connection_value (rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT); - if (NULL == mime) - mime = "application/json"; - if (0 == strcmp (mime, - "application/json")) - { - rctx->format = RCF_JSON; - rctx->details.json = json_array (); - GNUNET_assert (NULL != rctx->details.json); - } - else if (0 == strcmp (mime, - "application/pdf")) - { - enum GNUNET_DB_QueryStatus qs; - - rctx->format = RCF_PDF; - rctx->details.pdf.global_attrs = json_object (); - GNUNET_assert (NULL != rctx->details.pdf.global_attrs); - if (NULL != TEH_global_pdf_form_data) - GNUNET_assert (0 == - json_object_update (rctx->details.pdf.global_attrs, - TEH_global_pdf_form_data)); - GNUNET_assert (0 == - json_object_set_new (rctx->details.pdf.global_attrs, - "FILE_NUMBER", - json_integer (file_number))); - /* Lookup latest AML decision & account rules & properties - to build the cover page */ - qs = TEH_plugin->lookup_aml_history (TEH_plugin->cls, - &h_payto, - UINT64_MAX, /* offset */ - -1, /* latest decision only */ - &build_cover_page, - rctx); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_aml_attributes"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* no decision was ever taken, build empty cover page */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No decisions taken, creating empty cover page\n"); - build_cover_page (rctx, - 0, - GNUNET_TIME_UNIT_ZERO_TS, - NULL, - NULL, - NULL, - NULL, - false, - false); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - else - { - GNUNET_break_op (0); - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_NOT_ACCEPTABLE, - GNUNET_JSON_pack_string ("hint", - mime)); - } - } - - { - enum GNUNET_DB_QueryStatus qs; - - if (limit > MAX_RECORDS) - limit = MAX_RECORDS; - if (limit < -MAX_RECORDS) - limit = -MAX_RECORDS; - qs = TEH_plugin->select_aml_attributes ( - TEH_plugin->cls, - &h_payto, - offset, - limit, - &detail_cb, - rctx); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Obtained %d/%d attributes\n", - qs, - (int) (limit < 0 ? -limit : limit)); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_aml_attributes"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - if (RCF_JSON == rctx->format) - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - - switch (rctx->format) - { - case RCF_JSON: - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_incref ("details", - rctx->details.json)); - case RCF_PDF: - /* Cover page: off of 0 is impossible */ - GNUNET_assert (0 != rctx->details.pdf.off); - for (unsigned int i = 0; i<rctx->details.pdf.off; i++) - { - /* The jdata[i] could be a *string* in case of attachments, - ignore those here! */ - if (NULL == rctx->details.pdf.docs[i].form_name) - { - GNUNET_assert (json_is_string (rctx->details.pdf.jdata[i])); - } - else - { - GNUNET_assert (json_is_object (rctx->details.pdf.jdata[i])); - GNUNET_assert (0 == - json_object_update_missing ( - rctx->details.pdf.jdata[i], - rctx->details.pdf.global_attrs) - ); - } - } - rctx->tc = TALER_MHD_typst (TALER_EXCHANGE_project_data (), - TEH_cfg, - false, - "exchange", - rctx->details.pdf.off, - rctx->details.pdf.docs, - &pdf_cb, - rctx); - if (NULL == rctx->tc) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client requested PDF, but Typst is unavailable\n"); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK, - NULL); - } - GNUNET_CONTAINER_DLL_insert (rctx_head, - rctx_tail, - rctx); - MHD_suspend_connection (rc->connection); - return MHD_YES; - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-exchange-httpd_aml-attributes_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-attributes-get.h b/src/exchange/taler-exchange-httpd_aml-attributes-get.h @@ -1,52 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-attributes-get.h - * @brief Handle /aml/$OFFICER_PUB/attributes/$H_PAYTO requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AML_ATTRIBUTES_GET_H -#define TALER_EXCHANGE_HTTPD_AML_ATTRIBUTES_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/aml/$OFFICER_PUB/attributes/$H_PAYTO" request. Parses the request - * details, checks the signatures and if appropriately authorized returns - * the matching decisions. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be the state) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_attributes_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - - -/** - * Stop async running attribute GET requests. - */ -void -TEH_handler_aml_attributes_get_cleanup (void); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_aml-decision.c b/src/exchange/taler-exchange-httpd_aml-decision.c @@ -1,535 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-decision.c - * @brief Handle POST request about an AML decision. - * @author Christian Grothoff - * @author Florian Dold - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_aml-decision.h" - - -/** - * Context used for processing the AML decision request. - */ -struct AmlDecisionContext -{ - - /** - * Kept in a DLL. - */ - struct AmlDecisionContext *next; - - /** - * Kept in a DLL. - */ - struct AmlDecisionContext *prev; - - /** - * HTTP status code to use with @e response. - */ - unsigned int response_code; - - /** - * Response to return, NULL if none yet. - */ - struct MHD_Response *response; - - /** - * Request we are processing. - */ - struct TEH_RequestContext *rc; - - /** - * Handle for async KYC processing. - */ - struct TEH_KycMeasureRunContext *kat; - -}; - -/** - * Kept in a DLL. - */ -static struct AmlDecisionContext *adc_head; - -/** - * Kept in a DLL. - */ -static struct AmlDecisionContext *adc_tail; - - -void -TEH_aml_decision_cleanup () -{ - struct AmlDecisionContext *adc; - - while (NULL != (adc = adc_head)) - { - MHD_resume_connection (adc->rc->connection); - GNUNET_CONTAINER_DLL_remove (adc_head, - adc_tail, - adc); - } -} - - -/** - * Function called to clean up aml decision context. - * - * @param[in,out] rc context to clean up - */ -static void -aml_decision_cleaner (struct TEH_RequestContext *rc) -{ - struct AmlDecisionContext *adc = rc->rh_ctx; - - if (NULL != adc->kat) - { - TEH_kyc_run_measure_cancel (adc->kat); - adc->kat = NULL; - } - if (NULL != adc->response) - { - MHD_destroy_response (adc->response); - adc->response = NULL; - } - GNUNET_free (adc); -} - - -/** - * Function called after the KYC-AML trigger is done. - * - * @param cls closure - * @param ec error code or 0 on success - * @param detail error message or NULL on success / no info - */ -static void -aml_trigger_callback ( - void *cls, - enum TALER_ErrorCode ec, - const char *detail) -{ - struct AmlDecisionContext *adc = cls; - - adc->kat = NULL; - GNUNET_assert (NULL == adc->response); - - if (TALER_EC_NONE != ec) - { - adc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - adc->response = TALER_MHD_make_error ( - ec, - detail); - } - else - { - adc->response_code = MHD_HTTP_NO_CONTENT; - adc->response = MHD_create_response_from_buffer_static ( - 0, - ""); - TALER_MHD_add_global_headers (adc->response, - true); - } - - GNUNET_assert (NULL != adc->response); - - MHD_resume_connection (adc->rc->connection); - GNUNET_CONTAINER_DLL_remove (adc_head, - adc_tail, - adc); - TALER_MHD_daemon_trigger (); -} - - -MHD_RESULT -TEH_handler_post_aml_decision ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const json_t *root) -{ - struct MHD_Connection *connection = rc->connection; - struct AmlDecisionContext *adc = rc->rh_ctx; - const char *justification; - const char *new_measures = NULL; - bool to_investigate; - struct GNUNET_TIME_Timestamp decision_time; - struct GNUNET_TIME_Timestamp attributes_expiration - = GNUNET_TIME_UNIT_ZERO_TS; - const json_t *new_rules; - const json_t *events = NULL; - const json_t *properties = NULL; - const json_t *attributes = NULL; - struct TALER_FullPayto payto_uri = { - .full_payto = NULL - }; - struct TALER_NormalizedPaytoHashP h_payto; - struct TALER_AmlOfficerSignatureP officer_sig; - struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs = NULL; - uint64_t legi_measure_serial_id = 0; - MHD_RESULT ret; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ( - "new_measures", - &new_measures), - NULL), - GNUNET_JSON_spec_string ("justification", - &justification), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_full_payto_uri ("payto_uri", - &payto_uri), - NULL), - GNUNET_JSON_spec_fixed_auto ("h_payto", - &h_payto), - GNUNET_JSON_spec_object_const ("new_rules", - &new_rules), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("properties", - &properties), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("events", - &events), - NULL), - GNUNET_JSON_spec_bool ("keep_investigating", - &to_investigate), - GNUNET_JSON_spec_fixed_auto ("officer_sig", - &officer_sig), - GNUNET_JSON_spec_timestamp ("decision_time", - &decision_time), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("attributes_expiration", - &attributes_expiration), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("attributes", - &attributes), - NULL), - GNUNET_JSON_spec_end () - }; - struct GNUNET_TIME_Timestamp expiration_time; - json_t *jmeasures = NULL; - bool is_wallet; - size_t eas = 0; - void *ea = NULL; - - if (NULL == adc) - { - /* Initialize context */ - adc = GNUNET_new (struct AmlDecisionContext); - adc->rc = rc; - rc->rh_ctx = adc; - rc->rh_cleaner = aml_decision_cleaner; - } - - if (NULL != adc->response) - { - ret = MHD_queue_response (rc->connection, - adc->response_code, - adc->response); - goto done; - } - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - { - ret = MHD_NO; /* hard failure */ - goto done; - } - if (GNUNET_NO == res) - { - GNUNET_break_op (0); - ret = MHD_YES /* failure */; - goto done; - } - if ( (NULL != attributes) && - (! json_is_string (json_object_get (attributes, - "FORM_ID"))) ) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "attributes['FORM_ID']"); - goto done; - } - } - if (NULL != payto_uri.full_payto) - { - struct TALER_NormalizedPaytoHashP h_payto2; - - TALER_full_payto_normalize_and_hash (payto_uri, - &h_payto2); - if (0 != - GNUNET_memcmp (&h_payto, - &h_payto2)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payto_uri"); - goto done; - } - } - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_officer_aml_decision_verify ( - justification, - decision_time, - &h_payto, - new_rules, - properties, - new_measures, - to_investigate, - officer_pub, - &officer_sig, - attributes_expiration, - attributes)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID, - NULL); - goto done; - } - - lrs = TALER_KYCLOGIC_rules_parse (new_rules); - if (NULL == lrs) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "legitimization rule malformed"); - goto done; - } - - expiration_time = TALER_KYCLOGIC_rules_get_expiration (lrs); - if ( (NULL != new_measures) && - (0 != strlen (new_measures)) ) - { - jmeasures - = TALER_KYCLOGIC_get_jmeasures (lrs, - new_measures); - if (NULL == jmeasures) - { - GNUNET_break_op (0); - /* Request specified a new_measure for which the given - rule set does not work as it does not define the measure */ - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "new_measures/new_rules"); - goto done; - } - } - - { - size_t num_events = json_array_size (events); - const char *sevents[GNUNET_NZL (num_events)]; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp last_date; - bool invalid_officer = true; - bool unknown_account = false; - struct GNUNET_HashCode h_attr = { 0 }; - const char *form_id = NULL; - - if (NULL != attributes) - { - form_id = json_string_value (json_object_get (attributes, - "FORM_ID")); - TALER_json_hash (attributes, - &h_attr); - TALER_CRYPTO_kyc_attributes_encrypt (&TEH_attribute_key, - attributes, - &ea, - &eas); - } - - for (size_t i = 0; i<num_events; i++) - { - sevents[i] = json_string_value (json_array_get (events, - i)); - if (NULL == sevents[i]) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "events"); - goto done; - } - } - /* We keep 'new_measures' around mostly so that - the auditor can later verify officer_sig */ - qs = TEH_plugin->insert_aml_decision (TEH_plugin->cls, - payto_uri, - &h_payto, - decision_time, - expiration_time, - properties, - new_rules, - to_investigate, - new_measures, - jmeasures, - justification, - officer_pub, - &officer_sig, - num_events, - sevents, - form_id, - eas, /* enc_attributes_size*/ - ea, /* enc_attributes*/ - NULL != attributes - ? &h_attr - : NULL, /* attributes_hash */ - attributes_expiration, - &invalid_officer, - &unknown_account, - &last_date, - &legi_measure_serial_id, - &is_wallet); - json_decref (jmeasures); - if (qs <= 0) - { - GNUNET_break (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_aml_decision"); - goto done; - } - if (invalid_officer) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_AML_DECISION_INVALID_OFFICER, - NULL); - goto done; - } - if (unknown_account) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_BANK_ACCOUNT_UNKNOWN, - "h_payto"); - goto done; - } - if (GNUNET_TIME_timestamp_cmp (last_date, - >, - decision_time)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT, - NULL); - goto done; - } - } - /* Run instant measure if necessary */ - { - const struct TALER_KYCLOGIC_Measure *instant_ms = NULL; - - if (NULL != new_measures) - { - instant_ms = TALER_KYCLOGIC_get_instant_measure (lrs, - new_measures); - } - - if (NULL != instant_ms) - { - /* We have an 'instant' measure which means we must run the - AML program immediately instead of waiting for the account owner - to select some measure and contribute their KYC data. */ - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Running instant measure after AML decision\n"); - adc->kat = TEH_kyc_run_measure_directly ( - &rc->async_scope_id, - instant_ms, - &h_payto, - is_wallet, - &aml_trigger_callback, - adc - ); - if (NULL == adc->kat) - { - GNUNET_break (0); - ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG, - "TEH_kyc_finished"); - goto done; - } - MHD_suspend_connection (adc->rc->connection); - GNUNET_CONTAINER_DLL_insert (adc_head, - adc_tail, - adc); - ret = MHD_YES; - goto done; - } - } - ret = TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - goto done; - -done: - TALER_KYCLOGIC_rules_free (lrs); - GNUNET_free (ea); - return ret; -} - - -/* end of taler-exchange-httpd_aml-decision.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-decision.h b/src/exchange/taler-exchange-httpd_aml-decision.h @@ -1,67 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-decision.h - * @brief Handle /aml/$OFFICER_PUB/decision requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AML_DECISION_H -#define TALER_EXCHANGE_HTTPD_AML_DECISION_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a POST "/aml/$OFFICER_PUB/decision" request. Parses the decision - * details, checks the signatures and if appropriately authorized executes - * the decision. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_post_aml_decision ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const json_t *root); - - -/** - * Handle a GET "/aml/$OFFICER_PUB/decisions" request. Parses the request - * details, checks the signatures and if appropriately authorized returns - * the matching decisions. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be the state) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_decisions_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - -/** - * Clean up running POST /aml/$OFFICER_PUB/decisions requests. - */ -void -TEH_aml_decision_cleanup (void); - -#endif diff --git a/src/exchange/taler-exchange-httpd_aml-decisions-get.c b/src/exchange/taler-exchange-httpd_aml-decisions-get.c @@ -1,205 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024, 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-decisions-get.c - * @brief Return summary information about AML decisions - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler-exchange-httpd_aml-decision.h" -#include "taler-exchange-httpd_metrics.h" - -/** - * Maximum number of records we return in one request. - */ -#define MAX_RECORDS 1024 - -/** - * Return AML status. - * - * @param cls closure - * @param row_id current row in AML status table - * @param justification human-readable reason for the decision - * @param h_payto account for which the attribute data is stored - * @param decision_time when was the decision taken - * @param expiration_time when will the rules expire - * @param jproperties properties set for the account, - * NULL if no properties were set - * @param to_investigate true if AML staff should look at the account - * @param is_active true if this is the currently active decision about the account - * @param is_wallet true if the @a h_payto is for a wallet - * @param payto the payto URI of the account affected by the decision - * @param account_rules current active rules for the account - */ -static void -record_cb ( - void *cls, - uint64_t row_id, - const char *justification, - const struct TALER_NormalizedPaytoHashP *h_payto, - struct GNUNET_TIME_Timestamp decision_time, - struct GNUNET_TIME_Absolute expiration_time, - const json_t *jproperties, - bool to_investigate, - bool is_active, - bool is_wallet, - struct TALER_FullPayto payto, - const json_t *account_rules) -{ - json_t *records = cls; - - GNUNET_assert ( - 0 == - json_array_append_new ( - records, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("h_payto", - h_payto), - TALER_JSON_pack_full_payto ("full_payto", - payto), - GNUNET_JSON_pack_bool ("is_wallet", - is_wallet), - GNUNET_JSON_pack_int64 ("rowid", - row_id), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("justification", - justification)), - GNUNET_JSON_pack_timestamp ("decision_time", - decision_time), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("properties", - (json_t *) jproperties)), - GNUNET_JSON_pack_object_incref ("limits", - (json_t *) account_rules), - GNUNET_JSON_pack_bool ("to_investigate", - to_investigate), - GNUNET_JSON_pack_bool ("is_active", - is_active) - ))); -} - - -MHD_RESULT -TEH_handler_aml_decisions_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - int64_t limit = -20; - uint64_t offset; - struct TALER_NormalizedPaytoHashP h_payto; - bool have_payto = false; - enum TALER_EXCHANGE_YesNoAll active_filter; - enum TALER_EXCHANGE_YesNoAll investigation_filter; - - if (NULL != args[0]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - args[0]); - } - TALER_MHD_parse_request_snumber (rc->connection, - "limit", - &limit); - if (limit > 0) - offset = 0; - else - offset = INT64_MAX; - TALER_MHD_parse_request_number (rc->connection, - "offset", - &offset); - if (offset > INT64_MAX) - { - GNUNET_break_op (0); /* broken client */ - offset = INT64_MAX; - } - TALER_MHD_parse_request_arg_auto (rc->connection, - "h_payto", - &h_payto, - have_payto); - TALER_MHD_parse_request_yna (rc->connection, - "active", - TALER_EXCHANGE_YNA_ALL, - &active_filter); - TALER_MHD_parse_request_yna (rc->connection, - "investigation", - TALER_EXCHANGE_YNA_ALL, - &investigation_filter); - { - json_t *records; - enum GNUNET_DB_QueryStatus qs; - - records = json_array (); - GNUNET_assert (NULL != records); - if (limit > MAX_RECORDS) - limit = MAX_RECORDS; - if (limit < -MAX_RECORDS) - limit = -MAX_RECORDS; - qs = TEH_plugin->select_aml_decisions ( - TEH_plugin->cls, - have_payto - ? &h_payto - : NULL, - investigation_filter, - active_filter, - offset, - limit, - &record_cb, - records); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - json_decref (records); - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_aml_decisions"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - json_decref (records); - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("records", - records)); - } -} - - -/* end of taler-exchange-httpd_aml-decisions_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-legitimization-measures-get.c b/src/exchange/taler-exchange-httpd_aml-legitimization-measures-get.c @@ -1,180 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-legitimization-measures-get.c - * @brief Return information about legitimization measures - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler-exchange-httpd_aml-legitimization-measures-get.h" -#include "taler-exchange-httpd_metrics.h" - -/** - * Maximum number of measures we return in one request. - */ -#define MAX_MEASURES 1024 - -/** - * Return LEGITIMIZATION measure. - * - * @param cls closure - * @param h_payto hash of account the measure applies to - * @param start_time when was the process started - * @param jmeasures object of type ``LegitimizationMeasures`` - * @param is_finished true if the measure was finished - * @param measure_serial_id row ID of the measure in the exchange table - */ -static void -record_cb ( - void *cls, - struct TALER_NormalizedPaytoHashP *h_payto, - struct GNUNET_TIME_Absolute start_time, - const json_t *jmeasures, - bool is_finished, - uint64_t measure_serial_id) -{ - json_t *measures = cls; - - GNUNET_assert ( - 0 == - json_array_append_new ( - measures, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("h_payto", - h_payto), - GNUNET_JSON_pack_uint64 ("rowid", - measure_serial_id), - GNUNET_JSON_pack_timestamp ("start_time", - GNUNET_TIME_absolute_to_timestamp ( - start_time)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("measures", - (json_t *) jmeasures)), - GNUNET_JSON_pack_bool ("is_finished", - is_finished) - ))); -} - - -MHD_RESULT -TEH_handler_aml_legitimization_measures_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - int64_t limit = -20; - uint64_t offset; - struct TALER_NormalizedPaytoHashP h_payto; - bool have_payto = false; - enum TALER_EXCHANGE_YesNoAll active_filter; - - if (NULL != args[0]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - args[0]); - } - TALER_MHD_parse_request_snumber (rc->connection, - "limit", - &limit); - if (limit > 0) - offset = 0; - else - offset = INT64_MAX; - TALER_MHD_parse_request_number (rc->connection, - "offset", - &offset); - if (offset > INT64_MAX) - { - GNUNET_break_op (0); /* broken client */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Received invalid offset %llu > %llu\n", - (unsigned long long) offset, - (unsigned long long) INT64_MAX); - offset = INT64_MAX; - } - TALER_MHD_parse_request_arg_auto (rc->connection, - "h_payto", - &h_payto, - have_payto); - TALER_MHD_parse_request_yna (rc->connection, - "active", - TALER_EXCHANGE_YNA_ALL, - &active_filter); - { - json_t *measures; - enum GNUNET_DB_QueryStatus qs; - - measures = json_array (); - GNUNET_assert (NULL != measures); - if (limit > MAX_MEASURES) - limit = MAX_MEASURES; - if (limit < -MAX_MEASURES) - limit = -MAX_MEASURES; - qs = TEH_plugin->select_aml_measures ( - TEH_plugin->cls, - have_payto - ? &h_payto - : NULL, - active_filter, - offset, - limit, - &record_cb, - measures); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - json_decref (measures); - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_aml_measures"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - json_decref (measures); - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("measures", - measures)); - } -} - - -/* end of taler-exchange-httpd_legitimization-measures_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-legitimization-measures-get.h b/src/exchange/taler-exchange-httpd_aml-legitimization-measures-get.h @@ -1,43 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-legitimization-measures-get.h - * @brief Handle /aml/$OFFICER_PUB/legitimizations requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AML_LEGITIMIZATION_MEASURES_GET_H -#define TALER_EXCHANGE_HTTPD_AML_LEGITIMIZATION_MEASURES_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - -/** - * Handle a GET "/aml/$OFFICER_PUB/legitimizations" request. Parses the request - * details, checks the signatures and if appropriately authorized returns - * the matching measuress. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be the state) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_legitimization_measures_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_aml-measures-get.c b/src/exchange/taler-exchange-httpd_aml-measures-get.c @@ -1,75 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-measures-get.c - * @brief Return summary information about KYC measures - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd.h" -#include "taler-exchange-httpd_aml-measures-get.h" - - -MHD_RESULT -TEH_handler_aml_measures_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - static json_t *roots; - static json_t *programs; - static json_t *checks; - static json_t *default_rules; - - if (NULL == roots) - { - TALER_KYCLOGIC_get_measure_configuration (&roots, - &programs, - &checks, - &default_rules); - } - if (NULL != args[0]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - rc->url); - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_incref ("default_rules", - default_rules), - GNUNET_JSON_pack_object_incref ("roots", - roots), - GNUNET_JSON_pack_object_incref ("programs", - programs), - GNUNET_JSON_pack_object_incref ("checks", - checks)); -} - - -/* end of taler-exchange-httpd_aml-measures_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-measures-get.h b/src/exchange/taler-exchange-httpd_aml-measures-get.h @@ -1,45 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-measures-get.h - * @brief Handle /aml/$OFFICER_PUB/measures requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AML_MEASURES_GET_H -#define TALER_EXCHANGE_HTTPD_AML_MEASURES_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/aml/$OFFICER_PUB/measures" request. Parses the request - * details, checks the signatures and if appropriately authorized returns - * the matching decisions. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be empty) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_measures_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_aml-statistics-get.c b/src/exchange/taler-exchange-httpd_aml-statistics-get.c @@ -1,173 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-statistics-get.c - * @brief Return summary information about KYC statistics - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler-exchange-httpd_aml-statistics-get.h" -#include "taler-exchange-httpd_metrics.h" - -/** - * Maximum number of statistics that can be requested in one go. - */ -#define MAX_STATS 64 - - -/** - * Function called with AML statistics (counters). - * - * @param cls JSON array to build - * @param name name of the counter - * @param cnt number of events for @a name in the query range - */ -static void -add_stat ( - void *cls, - const char *name, - uint64_t cnt) -{ - json_t *stats = cls; - - GNUNET_assert ( - 0 == - json_array_append_new ( - stats, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("event", - name), - GNUNET_JSON_pack_uint64 ("counter", - cnt)))); -} - - -MHD_RESULT -TEH_handler_aml_kyc_statistics_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - struct GNUNET_TIME_Timestamp start_date = GNUNET_TIME_UNIT_ZERO_TS; - struct GNUNET_TIME_Timestamp end_date = GNUNET_TIME_UNIT_FOREVER_TS; - const char *all_names = args[0]; - json_t *stats; - size_t max_names; - - if ( (NULL == args[0]) || - (NULL != args[1]) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - rc->url); - } - TALER_MHD_parse_request_timestamp (rc->connection, - "start_date", - &start_date); - TALER_MHD_parse_request_timestamp (rc->connection, - "end_date", - &end_date); - if (GNUNET_TIME_absolute_is_never (end_date.abs_time)) - end_date = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_SECONDS)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Looking for stats on %s from [%llu,%llu)\n", - all_names, - (unsigned long long) start_date.abs_time.abs_value_us, - (unsigned long long) end_date.abs_time.abs_value_us); - /* Estimate number of names in 'all_names' */ - max_names = 1; - for (size_t i = 0; '\0' != all_names[i]; i++) - if (' ' == all_names[i]) - max_names++; - if (max_names > MAX_STATS) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_URI_TOO_LONG, - TALER_EC_GENERIC_URI_TOO_LONG, - rc->url); - } - stats = json_array (); - GNUNET_assert (NULL != stats); - { - char *buf; - const char *names[GNUNET_NZL (max_names)]; - enum GNUNET_DB_QueryStatus qs; - size_t num_names = 0; - - buf = GNUNET_strdup (all_names); - for (const char *tok = strtok (buf, - " "); - NULL != tok; - tok = strtok (NULL, - " ")) - { - GNUNET_assert (num_names < max_names); - names[num_names++] = tok; - } - qs = TEH_plugin->select_aml_statistics ( - TEH_plugin->cls, - num_names, - names, - start_date, - end_date, - &add_stat, - stats); - GNUNET_free (buf); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_aml_statistics"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("statistics", - stats)); - } -} - - -/* end of taler-exchange-httpd_aml-statistics_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-statistics-get.h b/src/exchange/taler-exchange-httpd_aml-statistics-get.h @@ -1,45 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-statistics-get.h - * @brief Handle /aml/$OFFICER_PUB/kyc-statistics/$NAME requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AML_STATISTICS_GET_H -#define TALER_EXCHANGE_HTTPD_AML_STATISTICS_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/aml/$OFFICER_PUB/kyc-statistics/$NAME" request. Parses the request - * details, checks the signatures and if appropriately authorized returns - * the matching decisions. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (name of the statistic) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_kyc_statistics_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_aml-transfer-get.c b/src/exchange/taler-exchange-httpd_aml-transfer-get.c @@ -1,258 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-transfer-get.c - * @brief Return information about exchange wire transfers - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler-exchange-httpd_aml-transfer-get.h" -#include "taler-exchange-httpd_metrics.h" - - -enum TransferType -{ - TT_CREDIT, - TT_DEBIT, - TT_KYCAUTH -}; - - -/** - * Maximum number of transfers we return in one request. - */ -#define MAX_TRANSFERS 1024 - -/** - * Return transfer data. - * - * @param cls closure - * @param row_id current row in AML status table - * @param payto_uri account involved with the wire transfer - * @param execution_time when was the transfer made - * @param amount wire amount of the transfer - */ -static void -record_cb ( - void *cls, - uint64_t row_id, - const char *payto_uri, - struct GNUNET_TIME_Absolute execution_time, - const struct TALER_Amount *amount) -{ - json_t *transfers = cls; - - GNUNET_assert ( - 0 == - json_array_append_new ( - transfers, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("payto_uri", - payto_uri), - GNUNET_JSON_pack_int64 ("rowid", - row_id), - GNUNET_JSON_pack_timestamp ("execution_time", - GNUNET_TIME_absolute_to_timestamp ( - execution_time)), - TALER_JSON_pack_amount ("amount", - amount) - ))); -} - - -/** - * Handle HTTP request by AML officer for transfer data. - * - * @param rc request context - * @param officer_pub the AML officer - * @param tt which type of transfer data to return - * @param args further arguments provided (should be empty) - * @return MHD status - */ -static MHD_RESULT -aml_transfer_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - enum TransferType tt, - const char *const args[]) -{ - int64_t limit = -20; - uint64_t offset; - struct TALER_Amount threshold; - struct TALER_NormalizedPaytoHashP h_payto; - bool have_payto = false; - - if (NULL != args[0]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - args[0]); - } - TALER_MHD_parse_request_arg_auto (rc->connection, - "h_payto", - &h_payto, - have_payto); - TALER_MHD_parse_request_snumber (rc->connection, - "limit", - &limit); - if (limit > 0) - offset = 0; - else - offset = INT64_MAX; - TALER_MHD_parse_request_number (rc->connection, - "offset", - &offset); - if (offset > INT64_MAX) - { - GNUNET_break_op (0); /* broken client */ - offset = INT64_MAX; - } - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &threshold)); - TALER_MHD_parse_request_amount (rc->connection, - "threshold", - &threshold); - { - json_t *transfers; - enum GNUNET_DB_QueryStatus qs; - const char *query; - - transfers = json_array (); - GNUNET_assert (NULL != transfers); - if (limit > MAX_TRANSFERS) - limit = MAX_TRANSFERS; - if (limit < -MAX_TRANSFERS) - limit = -MAX_TRANSFERS; - switch (tt) - { - case TT_DEBIT: - qs = TEH_plugin->select_exchange_debit_transfers ( - TEH_plugin->cls, - &threshold, - offset, - limit, - have_payto ? &h_payto : NULL, - &record_cb, - transfers); - query = "select_exchange_debit_transfers"; - break; - case TT_CREDIT: - qs = TEH_plugin->select_exchange_credit_transfers ( - TEH_plugin->cls, - &threshold, - offset, - limit, - have_payto ? &h_payto : NULL, - &record_cb, - transfers); - query = "select_exchange_credit_transfers"; - break; - case TT_KYCAUTH: - qs = TEH_plugin->select_exchange_kycauth_transfers ( - TEH_plugin->cls, - &threshold, - offset, - limit, - have_payto ? &h_payto : NULL, - &record_cb, - transfers); - query = "select_exchange_kycauth_transfers"; - break; - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - json_decref (transfers); - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - query); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - json_decref (transfers); - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("transfers", - transfers)); - } -} - - -MHD_RESULT -TEH_handler_aml_transfer_credit_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - return aml_transfer_get (rc, - officer_pub, - TT_CREDIT, - args); -} - - -MHD_RESULT -TEH_handler_aml_transfer_kycauth_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - return aml_transfer_get (rc, - officer_pub, - TT_KYCAUTH, - args); -} - - -MHD_RESULT -TEH_handler_aml_transfer_debit_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]) -{ - return aml_transfer_get (rc, - officer_pub, - TT_DEBIT, - args); -} - - -/* end of taler-exchange-httpd_aml-decisions_get.c */ diff --git a/src/exchange/taler-exchange-httpd_aml-transfer-get.h b/src/exchange/taler-exchange-httpd_aml-transfer-get.h @@ -1,79 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_aml-transfer-get.h - * @brief Handle /aml/$OFFICER_PUB/transfer-* requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AML_TRANSFER_GET_H -#define TALER_EXCHANGE_HTTPD_AML_TRANSFER_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/aml/$OFFICER_PUB/transfer-credit" request. Parses the - * request details, checks the signatures and if appropriately authorized - * returns the matching wire transfers. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be empty) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_transfer_credit_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - - -/** - * Handle a GET "/aml/$OFFICER_PUB/transfer-kycauth" request. Parses the - * request details, checks the signatures and if appropriately authorized - * returns the matching wire transfers. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be empty) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_transfer_kycauth_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - - -/** - * Handle a GET "/aml/$OFFICER_PUB/transfer-debit" request. Parses the - * request details, checks the signatures and if appropriately authorized - * returns the matching wire transfers. - * - * @param rc request context - * @param officer_pub public key of the AML officer who made the request - * @param args GET arguments (should be empty) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_aml_transfer_debit_get ( - struct TEH_RequestContext *rc, - const struct TALER_AmlOfficerPublicKeyP *officer_pub, - const char *const args[]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_auditors.c b/src/exchange/taler-exchange-httpd_auditors.c @@ -1,232 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_auditors.c - * @brief Handle request to add auditor signature on a denomination. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_auditors.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Closure for the #add_auditor_denom_sig transaction. - */ -struct AddAuditorDenomContext -{ - /** - * Auditor's signature affirming the AUDITORS XXX operation - * (includes timestamp). - */ - struct TALER_AuditorSignatureP auditor_sig; - - /** - * Denomination this is about. - */ - const struct TALER_DenominationHashP *h_denom_pub; - - /** - * Auditor this is about. - */ - const struct TALER_AuditorPublicKeyP *auditor_pub; - -}; - - -/** - * Function implementing database transaction to add an auditors. 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 closure with a `struct AddAuditorDenomContext` - * @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 -add_auditor_denom_sig (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct AddAuditorDenomContext *awc = cls; - struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; - enum GNUNET_DB_QueryStatus qs; - char *auditor_url; - bool enabled; - - qs = TEH_plugin->lookup_denomination_key ( - TEH_plugin->cls, - awc->h_denom_pub, - &meta); - 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, - "lookup denomination key"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, - GNUNET_h2s (&awc->h_denom_pub->hash)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - qs = TEH_plugin->lookup_auditor_status ( - TEH_plugin->cls, - awc->auditor_pub, - &auditor_url, - &enabled); - 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, - "lookup auditor"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_PRECONDITION_FAILED, - TALER_EC_EXCHANGE_AUDITORS_AUDITOR_UNKNOWN, - TALER_B2S (awc->auditor_pub)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! enabled) - { - GNUNET_free (auditor_url); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_GONE, - TALER_EC_EXCHANGE_AUDITORS_AUDITOR_INACTIVE, - TALER_B2S (awc->auditor_pub)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_auditor_denom_validity_verify ( - auditor_url, - awc->h_denom_pub, - &TEH_master_public_key, - meta.start, - meta.expire_withdraw, - meta.expire_deposit, - meta.expire_legal, - &meta.value, - &meta.fees, - awc->auditor_pub, - &awc->auditor_sig)) - { - GNUNET_free (auditor_url); - /* signature invalid */ - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - GNUNET_free (auditor_url); - - qs = TEH_plugin->insert_auditor_denom_sig (TEH_plugin->cls, - awc->h_denom_pub, - awc->auditor_pub, - &awc->auditor_sig); - if (qs < 0) - { - GNUNET_break (0); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "add auditor signature"); - return qs; - } - return qs; -} - - -MHD_RESULT -TEH_handler_auditors ( - struct MHD_Connection *connection, - const struct TALER_AuditorPublicKeyP *auditor_pub, - const struct TALER_DenominationHashP *h_denom_pub, - const json_t *root) -{ - struct AddAuditorDenomContext awc = { - .auditor_pub = auditor_pub, - .h_denom_pub = h_denom_pub - }; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("auditor_sig", - &awc.auditor_sig), - GNUNET_JSON_spec_end () - }; - MHD_RESULT res; - enum GNUNET_GenericReturnValue ret; - - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ - ret = TEH_DB_run_transaction (connection, - "add auditor denom sig", - TEH_MT_REQUEST_OTHER, - &res, - &add_auditor_denom_sig, - &awc); - if (GNUNET_SYSERR == ret) - return res; - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_auditors.c */ diff --git a/src/exchange/taler-exchange-httpd_auditors.h b/src/exchange/taler-exchange-httpd_auditors.h @@ -1,46 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_auditors.h - * @brief Handlers for the /auditors/ endpoints - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_AUDITORS_H -#define TALER_EXCHANGE_HTTPD_AUDITORS_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/auditors/$AUDITOR_PUB/$H_DENOM_PUB" request. - * - * @param connection the MHD connection to handle - * @param root uploaded JSON data - * @param auditor_pub public key of the auditor - * @param h_denom_pub hash of the denomination public key - * @return MHD result code - */ -MHD_RESULT -TEH_handler_auditors ( - struct MHD_Connection *connection, - const struct TALER_AuditorPublicKeyP *auditor_pub, - const struct TALER_DenominationHashP *h_denom_pub, - const json_t *root); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_batch-deposit.c b/src/exchange/taler-exchange-httpd_batch-deposit.c @@ -1,1207 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_batch-deposit.c - * @brief Handle /batch-deposit requests; parses the POST and JSON and - * verifies the coin signatures before handing things off - * to the database. - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_extensions_policy.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_batch-deposit.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_exchangedb_lib.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for #batch_deposit_transaction. - */ -struct BatchDepositContext -{ - - /** - * Kept in a DLL. - */ - struct BatchDepositContext *next; - - /** - * Kept in a DLL. - */ - struct BatchDepositContext *prev; - - /** - * The request we are working on. - */ - struct TEH_RequestContext *rc; - - /** - * Handle for the legitimization check. - */ - struct TEH_LegitimizationCheckHandle *lch; - - /** - * Array with the individual coin deposit fees. - */ - struct TALER_Amount *deposit_fees; - - /** - * Information about deposited coins. - */ - struct TALER_EXCHANGEDB_CoinDepositInformation *cdis; - - /** - * Additional details for policy extension relevant for this - * deposit operation, possibly NULL! - */ - json_t *policy_json; - - /** - * Response to return, if set. - */ - struct MHD_Response *response; - - /** - * KYC status of the reserve used for the operation. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - - /** - * Hash over @e policy_details, might be all zero - */ - struct TALER_ExtensionPolicyHashP h_policy; - - /** - * Hash over the merchant's payto://-URI with the wire salt. - */ - struct TALER_MerchantWireHashP h_wire; - - /** - * When @e policy_details are persisted, this contains the id of the record - * in the policy_details table. - */ - uint64_t policy_details_serial_id; - - /** - * Hash over the normalized payto://-URI of the account we are - * depositing into. - */ - struct TALER_NormalizedPaytoHashP nph; - - /** - * Our timestamp (when we received the request). - * Possibly updated by the transaction if the - * request is idempotent (was repeated). - */ - struct GNUNET_TIME_Timestamp exchange_timestamp; - - /** - * Total amount that is accumulated with this deposit, - * without fee. - */ - struct TALER_Amount accumulated_total_without_fee; - - /** - * Details about the batch deposit operation. - */ - struct TALER_EXCHANGEDB_BatchDeposit bd; - - /** - * If @e policy_json was present, the corresponding policy extension - * calculates these details. These will be persisted in the policy_details - * table. - */ - struct TALER_PolicyDetails policy_details; - - /** - * HTTP status to return with @e response, or 0. - */ - unsigned int http_status; - - /** - * Our current state in the state machine. - */ - enum - { - BDC_PHASE_INIT = 0, - BDC_PHASE_PARSE = 1, - BDC_PHASE_POLICY = 2, - BDC_PHASE_KYC = 3, - BDC_PHASE_TRANSACT = 4, - BDC_PHASE_REPLY_SUCCESS = 5, - BDC_PHASE_SUSPENDED, - BDC_PHASE_CHECK_KYC_RESULT, - BDC_PHASE_GENERATE_REPLY_FAILURE, - BDC_PHASE_RETURN_YES, - BDC_PHASE_RETURN_NO, - } phase; - - /** - * True, if no policy was present in the request. Then - * @e policy_json is NULL and @e h_policy will be all zero. - */ - bool has_no_policy; - - /** - * KYC failed because a KYC auth transfer is needed - * to establish the merchant_pub. - */ - bool bad_kyc_auth; -}; - - -/** - * Head of list of suspended batch deposit operations. - */ -static struct BatchDepositContext *bdc_head; - -/** - * Tail of list of suspended batch deposit operations. - */ -static struct BatchDepositContext *bdc_tail; - - -void -TEH_batch_deposit_cleanup () -{ - struct BatchDepositContext *bdc; - - while (NULL != (bdc = bdc_head)) - { - GNUNET_assert (BDC_PHASE_SUSPENDED == bdc->phase); - bdc->phase = BDC_PHASE_RETURN_NO; - MHD_resume_connection (bdc->rc->connection); - GNUNET_CONTAINER_DLL_remove (bdc_head, - bdc_tail, - bdc); - } -} - - -/** - * Terminate the main loop by returning the final - * result. - * - * @param[in,out] bdc context to update phase for - * @param mres MHD status to return - */ -static void -finish_loop (struct BatchDepositContext *bdc, - MHD_RESULT mres) -{ - bdc->phase = (MHD_YES == mres) - ? BDC_PHASE_RETURN_YES - : BDC_PHASE_RETURN_NO; -} - - -/** - * Send confirmation of batch deposit success to client. This function will - * create a signed message affirming the given information and return it to - * the client. By this, the exchange affirms that the coins had sufficient - * (residual) value for the specified transaction and that it will execute the - * requested batch deposit operation with the given wiring details. - * - * @param[in,out] bdc information about the batch deposit - */ -static void -bdc_phase_reply_success ( - struct BatchDepositContext *bdc) -{ - const struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; - const struct TALER_CoinSpendSignatureP *csigs[GNUNET_NZL (bd->num_cdis)]; - enum TALER_ErrorCode ec; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - - for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) - csigs[i] = &bd->cdis[i].csig; - if (TALER_EC_NONE != - (ec = TALER_exchange_online_deposit_confirmation_sign ( - &TEH_keys_exchange_sign_, - &bd->h_contract_terms, - &bdc->h_wire, - bdc->has_no_policy ? NULL : &bdc->h_policy, - bdc->exchange_timestamp, - bd->wire_deadline, - bd->refund_deadline, - &bdc->accumulated_total_without_fee, - bd->num_cdis, - csigs, - &bdc->bd.merchant_pub, - &pub, - &sig))) - { - GNUNET_break (0); - finish_loop (bdc, - TALER_MHD_reply_with_ec (bdc->rc->connection, - ec, - NULL)); - return; - } - finish_loop (bdc, - TALER_MHD_REPLY_JSON_PACK ( - bdc->rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_timestamp ("exchange_timestamp", - bdc->exchange_timestamp), - TALER_JSON_pack_amount ("accumulated_total_without_fee", - &bdc->accumulated_total_without_fee), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig))); -} - - -/** - * Execute database transaction for /batch-deposit. 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 BatchDepositContext` - * @param connection MHD request context - * @param[out] mhd_ret set to MHD status on error - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -batch_deposit_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct BatchDepositContext *bdc = cls; - const struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; - enum GNUNET_DB_QueryStatus qs = GNUNET_DB_STATUS_HARD_ERROR; - uint32_t bad_balance_coin_index = UINT32_MAX; - bool balance_ok; - bool in_conflict; - - /* If the deposit has a policy associated to it, persist it. This will - * insert or update the record. */ - if (! bdc->has_no_policy) - { - qs = TEH_plugin->persist_policy_details ( - TEH_plugin->cls, - &bdc->policy_details, - &bdc->bd.policy_details_serial_id, - &bdc->accumulated_total_without_fee, - &bdc->policy_details.fulfillment_state); - if (qs < 0) - return qs; - - bdc->bd.policy_blocked = - bdc->policy_details.fulfillment_state != TALER_PolicyFulfillmentSuccess; - } - - /* FIXME-#9373: replace by batch insert! */ - for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) - { - const struct TALER_EXCHANGEDB_CoinDepositInformation *cdi - = &bdc->cdis[i]; - uint64_t known_coin_id; - - qs = TEH_make_coin_known (&cdi->coin, - connection, - &known_coin_id, - mhd_ret); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "make coin known (%s) returned %d\n", - TALER_B2S (&cdi->coin.coin_pub), - qs); - if (qs < 0) - return qs; - } - qs = TEH_plugin->do_deposit ( - TEH_plugin->cls, - bd, - bdc->deposit_fees, - &bdc->exchange_timestamp, - &bdc->accumulated_total_without_fee, - &balance_ok, - &bad_balance_coin_index, - &in_conflict); - if (qs <= 0) - { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - TALER_LOG_WARNING ( - "Failed to store /batch-deposit information in database\n"); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "batch-deposit"); - return qs; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "do_deposit returned: %d / %s[%u] / %s\n", - qs, - balance_ok ? "balance ok" : "balance insufficient", - (unsigned int) bad_balance_coin_index, - in_conflict ? "in conflict" : "no conflict"); - if (in_conflict) - { - struct TALER_MerchantWireHashP h_wire; - - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - TEH_plugin->get_wire_hash_for_contract ( - TEH_plugin->cls, - &bd->merchant_pub, - &bd->h_contract_terms, - &h_wire)) - { - TALER_LOG_WARNING ( - "Failed to retrieve conflicting contract details from database\n"); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "batch-deposit"); - return qs; - } - - *mhd_ret - = TEH_RESPONSE_reply_coin_conflicting_contract ( - connection, - TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT, - &h_wire); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - GNUNET_assert (bad_balance_coin_index < bdc->bd.num_cdis); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "returning history of conflicting coin (%s)\n", - TALER_B2S (&bdc->cdis[bad_balance_coin_index].coin.coin_pub)); - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &bdc->cdis[bad_balance_coin_index].coin.denom_pub_hash, - &bdc->cdis[bad_balance_coin_index].coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT]++; - return qs; -} - - -/** - * Run database transaction. - * - * @param[in,out] bdc request context - */ -static void -bdc_phase_transact (struct BatchDepositContext *bdc) -{ - MHD_RESULT mhd_ret; - - if (GNUNET_SYSERR == - TEH_plugin->preflight (TEH_plugin->cls)) - { - GNUNET_break (0); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - "preflight failure")); - return; - } - - if (GNUNET_OK != - TEH_DB_run_transaction (bdc->rc->connection, - "execute batch deposit", - TEH_MT_REQUEST_BATCH_DEPOSIT, - &mhd_ret, - &batch_deposit_transaction, - bdc)) - { - finish_loop (bdc, - mhd_ret); - return; - } - bdc->phase++; -} - - -/** - * Check if the @a bdc is replayed and we already have an - * answer. If so, replay the existing answer and return the - * HTTP response. - * - * @param bdc parsed request data - * @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 -check_request_idempotent ( - struct BatchDepositContext *bdc) -{ - const struct TEH_RequestContext *rc = bdc->rc; - enum GNUNET_DB_QueryStatus qs; - bool is_idempotent; - - qs = TEH_plugin->do_check_deposit_idempotent ( - TEH_plugin->cls, - &bdc->bd, - &bdc->exchange_timestamp, - &is_idempotent); - if (0 > qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_check_deposit_idempotent")); - return true; - } - if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || - (! is_idempotent) ) - return false; - bdc->phase = BDC_PHASE_REPLY_SUCCESS; - return true; -} - - -/** - * Check the KYC result. - * - * @param bdc storage for request processing - */ -static void -bdc_phase_check_kyc_result (struct BatchDepositContext *bdc) -{ - /* return final positive response */ - if ( (! bdc->kyc.ok) || - (bdc->bad_kyc_auth) ) - { - if (check_request_idempotent (bdc)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Request is idempotent!\n"); - return; - } - /* KYC required */ - finish_loop (bdc, - TEH_RESPONSE_reply_kyc_required ( - bdc->rc->connection, - &bdc->nph, - &bdc->kyc, - bdc->bad_kyc_auth)); - return; - } - bdc->phase = BDC_PHASE_TRANSACT; -} - - -/** - * Function called with the result of a legitimization - * check. - * - * @param cls closure - * @param lcr legitimization check result - */ -static void -deposit_legi_cb ( - void *cls, - const struct TEH_LegitimizationCheckResult *lcr) -{ - struct BatchDepositContext *bdc = cls; - - bdc->lch = NULL; - GNUNET_assert (BDC_PHASE_SUSPENDED == - bdc->phase); - MHD_resume_connection (bdc->rc->connection); - GNUNET_CONTAINER_DLL_remove (bdc_head, - bdc_tail, - bdc); - TALER_MHD_daemon_trigger (); - if (NULL != lcr->response) - { - bdc->response = lcr->response; - bdc->http_status = lcr->http_status; - bdc->phase = BDC_PHASE_GENERATE_REPLY_FAILURE; - return; - } - bdc->kyc = lcr->kyc; - bdc->bad_kyc_auth = lcr->bad_kyc_auth; - bdc->phase = BDC_PHASE_CHECK_KYC_RESULT; -} - - -/** - * 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 - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -deposit_amount_cb ( - void *cls, - struct GNUNET_TIME_Absolute limit, - TALER_EXCHANGEDB_KycAmountCallback cb, - void *cb_cls) -{ - struct BatchDepositContext *bdc = cls; - enum GNUNET_GenericReturnValue ret; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Signaling amount %s for KYC check during deposit\n", - TALER_amount2s (&bdc->accumulated_total_without_fee)); - ret = cb (cb_cls, - &bdc->accumulated_total_without_fee, - bdc->exchange_timestamp.abs_time); - GNUNET_break (GNUNET_SYSERR != ret); - if (GNUNET_OK != ret) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - qs = TEH_plugin->select_deposit_amounts_for_kyc_check ( - TEH_plugin->cls, - &bdc->nph, - limit, - cb, - cb_cls); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got %d additional transactions for this deposit and limit %llu\n", - qs, - (unsigned long long) limit.abs_value_us); - GNUNET_break (qs >= 0); - return qs; -} - - -/** - * Run KYC check. - * - * @param[in,out] bdc request context - */ -static void -bdc_phase_kyc (struct BatchDepositContext *bdc) -{ - if (GNUNET_YES != TEH_enable_kyc) - { - bdc->phase++; - return; - } - TALER_full_payto_normalize_and_hash (bdc->bd.receiver_wire_account, - &bdc->nph); - bdc->lch = TEH_legitimization_check2 ( - &bdc->rc->async_scope_id, - TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, - bdc->bd.receiver_wire_account, - &bdc->nph, - &bdc->bd.merchant_pub, - &deposit_amount_cb, - bdc, - &deposit_legi_cb, - bdc); - GNUNET_assert (NULL != bdc->lch); - GNUNET_CONTAINER_DLL_insert (bdc_head, - bdc_tail, - bdc); - MHD_suspend_connection (bdc->rc->connection); - bdc->phase = BDC_PHASE_SUSPENDED; -} - - -/** - * Handle policy. - * - * @param[in,out] bdc request context - */ -static void -bdc_phase_policy (struct BatchDepositContext *bdc) -{ - const char *error_hint = NULL; - - if (bdc->has_no_policy) - { - bdc->phase++; - return; - } - if (GNUNET_OK != - TALER_extensions_create_policy_details ( - TEH_currency, - bdc->policy_json, - &bdc->policy_details, - &error_hint)) - { - GNUNET_break_op (0); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED, - error_hint)); - return; - } - - TALER_deposit_policy_hash (bdc->policy_json, - &bdc->h_policy); - bdc->phase++; -} - - -/** - * Parse per-coin deposit information from @a jcoin - * into @a deposit. Fill in generic information from - * @a ctx. - * - * @param bdc information about the overall batch - * @param jcoin coin data to parse - * @param[out] cdi where to store the result - * @param[out] deposit_fee where to write the deposit fee - * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, - * #GNUNET_SYSERR on failure and no error could be returned - */ -static enum GNUNET_GenericReturnValue -parse_coin (const struct BatchDepositContext *bdc, - json_t *jcoin, - struct TALER_EXCHANGEDB_CoinDepositInformation *cdi, - struct TALER_Amount *deposit_fee) -{ - const struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("contribution", - TEH_currency, - &cdi->amount_with_fee), - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &cdi->coin.denom_pub_hash), - TALER_JSON_spec_denom_sig ("ub_sig", - &cdi->coin.denom_sig), - GNUNET_JSON_spec_fixed_auto ("coin_pub", - &cdi->coin.coin_pub), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &cdi->coin.h_age_commitment), - &cdi->coin.no_age_commitment), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &cdi->csig), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - if (GNUNET_OK != - (res = TALER_MHD_parse_json_data (bdc->rc->connection, - jcoin, - spec))) - return res; - /* check denomination exists and is valid */ - { - struct TEH_DenominationKey *dk; - MHD_RESULT mret; - - dk = TEH_keys_denomination_by_hash ( - &cdi->coin.denom_pub_hash, - bdc->rc->connection, - &mret); - if (NULL == dk) - { - GNUNET_JSON_parse_free (spec); - return (MHD_YES == mret) - ? GNUNET_NO - : GNUNET_SYSERR; - } - if (0 > TALER_amount_cmp (&dk->meta.value, - &cdi->amount_with_fee)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE, - NULL)) - ? GNUNET_NO - : GNUNET_SYSERR; - } - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) - { - /* This denomination is past the expiration time for deposits */ - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TEH_RESPONSE_reply_expired_denom_pub_hash ( - bdc->rc->connection, - &cdi->coin.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "DEPOSIT")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) - { - /* This denomination is not yet valid */ - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TEH_RESPONSE_reply_expired_denom_pub_hash ( - bdc->rc->connection, - &cdi->coin.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "DEPOSIT")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - if (dk->recoup_possible) - { - /* This denomination has been revoked */ - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TEH_RESPONSE_reply_expired_denom_pub_hash ( - bdc->rc->connection, - &cdi->coin.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - "DEPOSIT")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - if (dk->denom_pub.bsign_pub_key->cipher != - cdi->coin.denom_sig.unblinded_sig->cipher) - { - /* denomination cipher and denomination signature cipher not the same */ - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL)) - ? GNUNET_NO - : GNUNET_SYSERR; - } - - *deposit_fee = dk->meta.fees.deposit; - /* check coin signature */ - switch (dk->denom_pub.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; - break; - case GNUNET_CRYPTO_BSA_CS: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; - break; - default: - break; - } - if (GNUNET_YES != - TALER_test_coin_valid (&cdi->coin, - &dk->denom_pub)) - { - TALER_LOG_WARNING ("Invalid coin passed for /batch-deposit\n"); - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL)) - ? GNUNET_NO - : GNUNET_SYSERR; - } - } - if (0 < TALER_amount_cmp (deposit_fee, - &cdi->amount_with_fee)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE, - NULL)) - ? GNUNET_NO - : GNUNET_SYSERR; - } - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_deposit_verify ( - &cdi->amount_with_fee, - deposit_fee, - &bdc->h_wire, - &bd->h_contract_terms, - &bd->wallet_data_hash, - cdi->coin.no_age_commitment - ? NULL - : &cdi->coin.h_age_commitment, - NULL != bdc->policy_json ? &bdc->h_policy : NULL, - &cdi->coin.denom_pub_hash, - bd->wallet_timestamp, - &bd->merchant_pub, - bd->refund_deadline, - &cdi->coin.coin_pub, - &cdi->csig)) - { - TALER_LOG_WARNING ("Invalid signature on /batch-deposit request\n"); - GNUNET_JSON_parse_free (spec); - return (MHD_YES == - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID, - TALER_B2S (&cdi->coin.coin_pub))) - ? GNUNET_NO - : GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Run processing phase that parses the request. - * - * @param[in,out] bdc request context - * @param root JSON object that was POSTed - */ -static void -bdc_phase_parse (struct BatchDepositContext *bdc, - const json_t *root) -{ - struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; - const json_t *coins; - const json_t *policy_json; - bool no_refund_deadline = true; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_full_payto_uri ("merchant_payto_uri", - &bd->receiver_wire_account), - GNUNET_JSON_spec_fixed_auto ("wire_salt", - &bd->wire_salt), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &bd->merchant_pub), - GNUNET_JSON_spec_fixed_auto ("merchant_sig", - &bd->merchant_sig), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &bd->h_contract_terms), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("wallet_data_hash", - &bd->wallet_data_hash), - &bd->no_wallet_data_hash), - GNUNET_JSON_spec_array_const ("coins", - &coins), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("policy", - &policy_json), - &bdc->has_no_policy), - GNUNET_JSON_spec_timestamp ("timestamp", - &bd->wallet_timestamp), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("extra_wire_subject_metadata", - &bd->extra_wire_subject_metadata), - &bd->no_wallet_data_hash), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("refund_deadline", - &bd->refund_deadline), - &no_refund_deadline), - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &bd->wire_deadline), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (bdc->rc->connection, - root, - spec); - if (GNUNET_SYSERR == res) - { - /* hard failure */ - GNUNET_break (0); - finish_loop (bdc, - MHD_NO); - return; - } - if (GNUNET_NO == res) - { - /* failure */ - GNUNET_break_op (0); - finish_loop (bdc, - MHD_YES); - return; - } - } - if (! TALER_is_valid_subject_metadata_string ( - bd->extra_wire_subject_metadata)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "extra_wire_subject_metadata")); - return; - } - if (GNUNET_OK != - TALER_merchant_contract_verify ( - &bd->h_contract_terms, - &bd->merchant_pub, - &bd->merchant_sig)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "merchant_sig")); - return; - } - bdc->policy_json - = json_incref ((json_t *) policy_json); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Batch deposit into contract %s\n", - GNUNET_h2s (&bd->h_contract_terms.hash)); - - /* validate merchant's wire details (as far as we can) */ - { - char *emsg; - - emsg = TALER_payto_validate (bd->receiver_wire_account); - if (NULL != emsg) - { - MHD_RESULT ret; - - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - ret = TALER_MHD_reply_with_error (bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - emsg); - GNUNET_free (emsg); - finish_loop (bdc, - ret); - return; - } - } - if (GNUNET_TIME_timestamp_cmp (bd->refund_deadline, - >, - bd->wire_deadline)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE, - NULL)); - return; - } - if (GNUNET_TIME_absolute_is_never (bd->wire_deadline.abs_time)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER, - NULL)); - return; - } - TALER_full_payto_hash (bd->receiver_wire_account, - &bd->wire_target_h_payto); - TALER_merchant_wire_signature_hash (bd->receiver_wire_account, - &bd->wire_salt, - &bdc->h_wire); - bd->num_cdis = json_array_size (coins); - if (0 == bd->num_cdis) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "coins")); - return; - } - if (TALER_MAX_COINS < bd->num_cdis) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - finish_loop (bdc, - TALER_MHD_reply_with_error ( - bdc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "coins")); - return; - } - - bdc->cdis - = GNUNET_new_array (bd->num_cdis, - struct TALER_EXCHANGEDB_CoinDepositInformation); - bdc->deposit_fees - = GNUNET_new_array (bd->num_cdis, - struct TALER_Amount); - bd->cdis = bdc->cdis; - for (unsigned i = 0; i<bd->num_cdis; i++) - { - struct TALER_Amount amount_without_fee; - enum GNUNET_GenericReturnValue res; - - res = parse_coin (bdc, - json_array_get (coins, - i), - &bdc->cdis[i], - &bdc->deposit_fees[i]); - if (GNUNET_OK != res) - { - finish_loop (bdc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - GNUNET_assert (0 <= - TALER_amount_subtract ( - &amount_without_fee, - &bdc->cdis[i].amount_with_fee, - &bdc->deposit_fees[i])); - - GNUNET_assert (0 <= - TALER_amount_add ( - &bdc->accumulated_total_without_fee, - &bdc->accumulated_total_without_fee, - &amount_without_fee)); - } - - GNUNET_JSON_parse_free (spec); - bdc->phase++; -} - - -/** - * Function called to clean up a context. - * - * @param rc request context with data to clean up - */ -static void -bdc_cleaner (struct TEH_RequestContext *rc) -{ - struct BatchDepositContext *bdc = rc->rh_ctx; - - if (NULL != bdc->lch) - { - TEH_legitimization_check_cancel (bdc->lch); - bdc->lch = NULL; - } - if (0 != bdc->cdis) - { - for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) - TALER_denom_sig_free (&bdc->cdis[i].coin.denom_sig); - GNUNET_free (bdc->cdis); - } - GNUNET_free (bdc->deposit_fees); - json_decref (bdc->policy_json); - GNUNET_free (bdc); -} - - -MHD_RESULT -TEH_handler_batch_deposit (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[]) -{ - struct BatchDepositContext *bdc = rc->rh_ctx; - - (void) args; - if (NULL == bdc) - { - bdc = GNUNET_new (struct BatchDepositContext); - bdc->rc = rc; - rc->rh_ctx = bdc; - rc->rh_cleaner = &bdc_cleaner; - bdc->phase = BDC_PHASE_PARSE; - bdc->exchange_timestamp = GNUNET_TIME_timestamp_get (); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &bdc->accumulated_total_without_fee)); - } - while (1) - { - switch (bdc->phase) - { - case BDC_PHASE_INIT: - GNUNET_break (0); - bdc->phase = BDC_PHASE_RETURN_NO; - break; - case BDC_PHASE_PARSE: - bdc_phase_parse (bdc, - root); - break; - case BDC_PHASE_POLICY: - bdc_phase_policy (bdc); - break; - case BDC_PHASE_KYC: - bdc_phase_kyc (bdc); - break; - case BDC_PHASE_TRANSACT: - bdc_phase_transact (bdc); - break; - case BDC_PHASE_REPLY_SUCCESS: - bdc_phase_reply_success (bdc); - break; - case BDC_PHASE_SUSPENDED: - return MHD_YES; - case BDC_PHASE_CHECK_KYC_RESULT: - bdc_phase_check_kyc_result (bdc); - break; - case BDC_PHASE_GENERATE_REPLY_FAILURE: - return MHD_queue_response (bdc->rc->connection, - bdc->http_status, - bdc->response); - case BDC_PHASE_RETURN_YES: - return MHD_YES; - case BDC_PHASE_RETURN_NO: - return MHD_NO; - } - } -} - - -/* end of taler-exchange-httpd_batch-deposit.c */ diff --git a/src/exchange/taler-exchange-httpd_batch-deposit.h b/src/exchange/taler-exchange-httpd_batch-deposit.h @@ -1,57 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_batch-deposit.h - * @brief Handle /batch-deposit requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_BATCH_DEPOSIT_H -#define TALER_EXCHANGE_HTTPD_BATCH_DEPOSIT_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Resumes all suspended batch deposit requests - * during cleanup. - */ -void -TEH_batch_deposit_cleanup (void); - - -/** - * Handle a "/batch-deposit" request. Parses the JSON, and, if - * successful, passes the JSON data to #deposit_transaction() to - * further check the details of the operation specified. If everything checks - * out, this will ultimately lead to the "/batch-deposit" being executed, or - * rejected. - * - * @param rc request context - * @param root uploaded JSON data - * @param args arguments, empty in this case - * @return MHD result code - */ -MHD_RESULT -TEH_handler_batch_deposit (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_blinding-prepare.c b/src/exchange/taler-exchange-httpd_blinding-prepare.c @@ -1,223 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_blinding-prepare.c - * @brief Handle /blinding-prepare requests - * @author Özgür Kesim - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_blinding-prepare.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - -MHD_RESULT -TEH_handler_blinding_prepare (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[]) -{ - struct TALER_BlindingMasterSeedP blinding_seed; - const char *cipher; - const char *operation; - const json_t *j_nks; - size_t num; - bool is_melt; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("cipher", - &cipher), - GNUNET_JSON_spec_string ("operation", - &operation), - GNUNET_JSON_spec_fixed_auto ("seed", - &blinding_seed), - GNUNET_JSON_spec_array_const ("nks", - &j_nks), - GNUNET_JSON_spec_end () - }; - - (void) args; - { - 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; - } - if (0 == strcmp (operation, - "melt")) - { - is_melt = true; - } - else if (0 == strcmp (operation, - "withdraw")) - { - is_melt = false; - } - else - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "operation"); - } - - num = json_array_size (j_nks); - if ((0 == num) || - (TALER_MAX_COINS < num)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE, - "nks"); - } - if (0 != strcmp (cipher, "CS")) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - " cipher"); - } - - { - uint32_t cs_indices[num]; - struct TALER_DenominationHashP h_denom_pubs[num]; - struct GNUNET_CRYPTO_CsSessionNonce nonces[num]; - - for (size_t i = 0; i < num; i++) - { - enum GNUNET_GenericReturnValue res; - struct GNUNET_JSON_Specification nks_spec[] = { - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &h_denom_pubs[i]), - GNUNET_JSON_spec_uint32 ("coin_offset", - &cs_indices[i]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_array (rc->connection, - j_nks, - nks_spec, - i, - -1); - if (GNUNET_OK != res) - return (GNUNET_SYSERR == res) - ? MHD_NO - : MHD_YES; - - if (TALER_MAX_COINS < cs_indices[i]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "nks"); - } - } - - TALER_cs_derive_nonces_from_seed (&blinding_seed, - is_melt, - num, - cs_indices, - nonces); - { - struct TEH_KeyStateHandle *ksh; - struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[num]; - size_t err_idx; - enum TALER_ErrorCode ec; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - - ec = - TEH_keys_denomination_cs_batch_r_pub (ksh, - num, - h_denom_pubs, - nonces, - is_melt, - r_pubs, - &err_idx); - switch (ec) - { - case TALER_EC_NONE: - break; - case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN: - return TEH_RESPONSE_reply_unknown_denom_pub_hash ( - rc->connection, - &h_denom_pubs[err_idx]); - break; - case TALER_EC_EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION: - return TEH_RESPONSE_reply_invalid_denom_cipher_for_operation ( - rc->connection, - &h_denom_pubs[err_idx]); - break; - case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED: - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &h_denom_pubs[err_idx], - ec, - "blinding-prepare"); - break; - case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE: - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &h_denom_pubs[err_idx], - ec, - "blinding-prepare"); - break; - default: - GNUNET_break (0); - return TALER_MHD_reply_with_ec (rc->connection, - ec, - NULL); - break; - } - - /* Finally, create the response */ - { - struct TALER_BlindingPrepareResponse response = { - .num = num, - .cipher = GNUNET_CRYPTO_BSA_CS, - .details.cs = r_pubs, - }; - - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - TALER_JSON_pack_blinding_prepare_response (NULL, - &response)); - } - } - } -} - - -/* end of taler-exchange-httpd_blinding_prepare.c */ diff --git a/src/exchange/taler-exchange-httpd_blinding-prepare.h b/src/exchange/taler-exchange-httpd_blinding-prepare.h @@ -1,40 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_blinding-prepare.h - * @brief Handle /blinding-prepare requests - * @author Özgür Kesim - */ -#ifndef TALER_EXCHANGE_HTTPD_BLINDING_PREPARE_H -#define TALER_EXCHANGE_HTTPD_BLINDING_PREPARE_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - -/** - * Handle a "/blinding-prepare" request. - * - * @param rc request context - * @param root uploaded JSON data - * @param args empty array - * @return MHD result code - */ -MHD_RESULT -TEH_handler_blinding_prepare (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_coins_get.c b/src/exchange/taler-exchange-httpd_coins_get.c @@ -1,763 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_coins_get.c - * @brief Handle GET /coins/$COIN_PUB/history requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_mhd_lib.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_coins_get.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Add the headers we want to set for every response. - * - * @param cls the key state to use - * @param[in,out] response the response to modify - */ -static void -add_response_headers (void *cls, - struct MHD_Response *response) -{ - (void) cls; - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_CACHE_CONTROL, - "no-cache")); -} - - -/** - * Compile the transaction history of a coin into a JSON object. - * - * @param coin_pub public key of the coin - * @param tl transaction history to JSON-ify - * @return json representation of the @a rh, NULL on error - */ -static json_t * -compile_transaction_history ( - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_EXCHANGEDB_TransactionList *tl) -{ - json_t *history; - - history = json_array (); - if (NULL == history) - { - GNUNET_break (0); /* out of memory!? */ - return NULL; - } - for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; - NULL != pos; - pos = pos->next) - { - switch (pos->type) - { - case TALER_EXCHANGEDB_TT_DEPOSIT: - { - const struct TALER_EXCHANGEDB_DepositListEntry *deposit = - pos->details.deposit; - struct TALER_MerchantWireHashP h_wire; - - TALER_merchant_wire_signature_hash (deposit->receiver_wire_account, - &deposit->wire_salt, - &h_wire); -#if ENABLE_SANITY_CHECKS - /* internal sanity check before we hand out a bogus sig... */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_deposit_verify ( - &deposit->amount_with_fee, - &deposit->deposit_fee, - &h_wire, - &deposit->h_contract_terms, - deposit->no_wallet_data_hash - ? NULL - : &deposit->wallet_data_hash, - deposit->no_age_commitment - ? NULL - : &deposit->h_age_commitment, - &deposit->h_policy, - &deposit->h_denom_pub, - deposit->timestamp, - &deposit->merchant_pub, - deposit->refund_deadline, - coin_pub, - &deposit->csig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } -#endif - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "DEPOSIT"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &deposit->amount_with_fee), - TALER_JSON_pack_amount ("deposit_fee", - &deposit->deposit_fee), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &deposit->merchant_pub), - GNUNET_JSON_pack_timestamp ("timestamp", - deposit->timestamp), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("refund_deadline", - deposit->refund_deadline)), - GNUNET_JSON_pack_data_auto ("h_contract_terms", - &deposit->h_contract_terms), - GNUNET_JSON_pack_data_auto ("h_wire", - &h_wire), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &deposit->h_denom_pub), - GNUNET_JSON_pack_allow_null ( - deposit->has_policy - ? GNUNET_JSON_pack_data_auto ("h_policy", - &deposit->h_policy) - : GNUNET_JSON_pack_string ( - "h_policy", - NULL)), - GNUNET_JSON_pack_allow_null ( - deposit->no_wallet_data_hash - ? GNUNET_JSON_pack_string ( - "wallet_data_hash", - NULL) - : GNUNET_JSON_pack_data_auto ("wallet_data_hash", - &deposit->wallet_data_hash)), - GNUNET_JSON_pack_allow_null ( - deposit->no_age_commitment - ? GNUNET_JSON_pack_string ( - "h_age_commitment", - NULL) - : GNUNET_JSON_pack_data_auto ("h_age_commitment", - &deposit->h_age_commitment)), - GNUNET_JSON_pack_data_auto ("coin_sig", - &deposit->csig)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - case TALER_EXCHANGEDB_TT_MELT: - { - const struct TALER_EXCHANGEDB_MeltListEntry *melt = - pos->details.melt; - const struct TALER_AgeCommitmentHashP *phac; - const struct TALER_BlindingMasterSeedP *pbs; - -#if ENABLE_SANITY_CHECKS - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_melt_verify ( - &melt->amount_with_fee, - &melt->melt_fee, - &melt->rc, - &melt->h_denom_pub, - &melt->h_age_commitment, - coin_pub, - &melt->coin_sig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } -#endif - phac = (melt->no_age_commitment) - ? NULL - : &melt->h_age_commitment; - pbs = melt->no_blinding_seed - ? NULL - : &melt->blinding_seed; - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "MELT"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &melt->amount_with_fee), - TALER_JSON_pack_amount ("melt_fee", - &melt->melt_fee), - GNUNET_JSON_pack_data_auto ("rc", - &melt->rc), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &melt->h_denom_pub), - GNUNET_JSON_pack_data_auto ("refresh_seed", - &melt->refresh_seed), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_data_auto ("blinding_seed", - pbs)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_data_auto ("h_age_commitment", - phac)), - GNUNET_JSON_pack_data_auto ("coin_sig", - &melt->coin_sig)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_TT_REFUND: - { - const struct TALER_EXCHANGEDB_RefundListEntry *refund = - pos->details.refund; - struct TALER_Amount value; - -#if ENABLE_SANITY_CHECKS - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_merchant_refund_verify ( - coin_pub, - &refund->h_contract_terms, - refund->rtransaction_id, - &refund->refund_amount, - &refund->merchant_pub, - &refund->merchant_sig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } -#endif - if (0 > - TALER_amount_subtract (&value, - &refund->refund_amount, - &refund->refund_fee)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "REFUND"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &value), - TALER_JSON_pack_amount ("refund_fee", - &refund->refund_fee), - GNUNET_JSON_pack_data_auto ("h_contract_terms", - &refund->h_contract_terms), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &refund->merchant_pub), - GNUNET_JSON_pack_uint64 ("rtransaction_id", - refund->rtransaction_id), - GNUNET_JSON_pack_data_auto ("merchant_sig", - &refund->merchant_sig)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER: - { - struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = - pos->details.old_coin_recoup; - struct TALER_ExchangePublicKeyP epub; - struct TALER_ExchangeSignatureP esig; - - if (TALER_EC_NONE != - TALER_exchange_online_confirm_recoup_refresh_sign ( - &TEH_keys_exchange_sign_, - pr->timestamp, - &pr->value, - &pr->coin.coin_pub, - &pr->old_coin_pub, - &epub, - &esig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - /* NOTE: we could also provide coin_pub's coin_sig, denomination key hash and - the denomination key's RSA signature over coin_pub, but as the - wallet should really already have this information (and cannot - check or do anything with it anyway if it doesn't), it seems - strictly unnecessary. */ - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RECOUP-REFRESH-RECEIVER"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &pr->value), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &esig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &epub), - GNUNET_JSON_pack_data_auto ("coin_pub", - &pr->coin.coin_pub), - GNUNET_JSON_pack_timestamp ("timestamp", - pr->timestamp)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - case TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW: - { - const struct TALER_EXCHANGEDB_RecoupListEntry *recoup = - pos->details.recoup; - struct TALER_ExchangePublicKeyP epub; - struct TALER_ExchangeSignatureP esig; - - if (TALER_EC_NONE != - TALER_exchange_online_confirm_recoup_sign ( - &TEH_keys_exchange_sign_, - recoup->timestamp, - &recoup->value, - coin_pub, - &recoup->reserve_pub, - &epub, - &esig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RECOUP-WITHDRAW"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &recoup->value), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &esig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &epub), - GNUNET_JSON_pack_data_auto ("reserve_pub", - &recoup->reserve_pub), - GNUNET_JSON_pack_data_auto ("coin_sig", - &recoup->coin_sig), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &recoup->h_denom_pub), - GNUNET_JSON_pack_data_auto ("coin_blind", - &recoup->coin_blind), - // FIXME-9828: spec says we should have h_commitment? - // FIXME-9828: spec says we should have coin_index? - GNUNET_JSON_pack_data_auto ("reserve_pub", - &recoup->reserve_pub), - GNUNET_JSON_pack_timestamp ("timestamp", - recoup->timestamp)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: - { - struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = - pos->details.recoup_refresh; - struct TALER_ExchangePublicKeyP epub; - struct TALER_ExchangeSignatureP esig; - - if (TALER_EC_NONE != - TALER_exchange_online_confirm_recoup_refresh_sign ( - &TEH_keys_exchange_sign_, - pr->timestamp, - &pr->value, - coin_pub, - &pr->old_coin_pub, - &epub, - &esig)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - /* NOTE: we could also provide coin_pub's coin_sig, denomination key - hash and the denomination key's RSA signature over coin_pub, but as - the wallet should really already have this information (and cannot - check or do anything with it anyway if it doesn't), it seems - strictly unnecessary. */ - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RECOUP-REFRESH"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &pr->value), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &esig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &epub), - GNUNET_JSON_pack_data_auto ("old_coin_pub", - &pr->old_coin_pub), - GNUNET_JSON_pack_data_auto ("coin_sig", - &pr->coin_sig), - // FIXME-#9828: spec says to return h_denom_pub - GNUNET_JSON_pack_data_auto ("coin_blind", - &pr->coin_blind), - // FIXME-#9828: spec says to return h_commitment - // FIXME-#9828: spec says to return coin_index - // FIXME-#9828: spec says to return new_coin_blinding_secret - // FIXME-#9828: spec says to return new_coin_ev - GNUNET_JSON_pack_timestamp ("timestamp", - pr->timestamp)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - - case TALER_EXCHANGEDB_TT_PURSE_DEPOSIT: - { - struct TALER_EXCHANGEDB_PurseDepositListEntry *pd - = pos->details.purse_deposit; - const struct TALER_AgeCommitmentHashP *phac = NULL; - - if (! pd->no_age_commitment) - phac = &pd->h_age_commitment; - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "PURSE-DEPOSIT"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &pd->amount), - GNUNET_JSON_pack_string ("exchange_base_url", - NULL == pd->exchange_base_url - ? TEH_base_url - : pd->exchange_base_url), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_data_auto ("h_age_commitment", - phac)), - TALER_JSON_pack_amount ("deposit_fee", - &pd->deposit_fee), - GNUNET_JSON_pack_data_auto ("purse_pub", - &pd->purse_pub), - GNUNET_JSON_pack_bool ("refunded", - pd->refunded), - GNUNET_JSON_pack_data_auto ("coin_sig", - &pd->coin_sig), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &pd->h_denom_pub)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - - case TALER_EXCHANGEDB_TT_PURSE_REFUND: - { - const struct TALER_EXCHANGEDB_PurseRefundListEntry *prefund = - pos->details.purse_refund; - struct TALER_Amount value; - enum TALER_ErrorCode ec; - struct TALER_ExchangePublicKeyP epub; - struct TALER_ExchangeSignatureP esig; - - if (0 > - TALER_amount_subtract (&value, - &prefund->refund_amount, - &prefund->refund_fee)) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - ec = TALER_exchange_online_purse_refund_sign ( - &TEH_keys_exchange_sign_, - &value, - &prefund->refund_fee, - coin_pub, - &prefund->purse_pub, - &epub, - &esig); - if (TALER_EC_NONE != ec) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "PURSE-REFUND"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("amount", - &value), - TALER_JSON_pack_amount ("refund_fee", - &prefund->refund_fee), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &esig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &epub), - GNUNET_JSON_pack_data_auto ("purse_pub", - &prefund->purse_pub)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - } - break; - - case TALER_EXCHANGEDB_TT_RESERVE_OPEN: - { - struct TALER_EXCHANGEDB_ReserveOpenListEntry *role - = pos->details.reserve_open; - - if (0 != - json_array_append_new ( - history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RESERVE-OPEN-DEPOSIT"), - GNUNET_JSON_pack_uint64 ("history_offset", - pos->coin_history_id), - TALER_JSON_pack_amount ("coin_contribution", - &role->coin_contribution), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &role->reserve_sig), - GNUNET_JSON_pack_data_auto ("coin_sig", - &role->coin_sig)))) - { - GNUNET_break (0); - json_decref (history); - return NULL; - } - break; - } - } - } - return history; -} - - -MHD_RESULT -TEH_handler_coins_get (struct TEH_RequestContext *rc, - const struct TALER_CoinSpendPublicKeyP *coin_pub) -{ - struct TALER_EXCHANGEDB_TransactionList *tl = NULL; - uint64_t start_off = 0; - uint64_t etag_in; - uint64_t etag_out; - char etagp[24]; - struct MHD_Response *resp; - unsigned int http_status; - struct TALER_DenominationHashP h_denom_pub; - struct TALER_Amount balance; - - TALER_MHD_parse_request_number (rc->connection, - "start", - &start_off); - /* Check signature */ - { - struct TALER_CoinSpendSignatureP coin_sig; - bool required = true; - - TALER_MHD_parse_request_header_auto (rc->connection, - TALER_COIN_HISTORY_SIGNATURE_HEADER, - &coin_sig, - required); - if (GNUNET_OK != - TALER_wallet_coin_history_verify (start_off, - coin_pub, - &coin_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_COIN_HISTORY_BAD_SIGNATURE, - NULL); - } - } - - /* Get etag */ - { - const char *etags; - - etags = MHD_lookup_connection_value (rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if (NULL != etags) - { - char dummy; - unsigned long long ev; - - if (1 != sscanf (etags, - "\"%llu\"%c", - &ev, - &dummy)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client send malformed `If-None-Match' header `%s'\n", - etags); - etag_in = start_off; - } - else - { - etag_in = (uint64_t) ev; - } - } - else - { - etag_in = start_off; - } - } - - /* Get history from DB between etag and now */ - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - true, - coin_pub, - start_off, - etag_in, - &etag_out, - &balance, - &h_denom_pub, - &tl); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_coin_history"); - case GNUNET_DB_STATUS_SOFT_ERROR: - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "get_coin_history"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* Handled below */ - break; - } - } - - GNUNET_snprintf (etagp, - sizeof (etagp), - "\"%llu\"", - (unsigned long long) etag_out); - if (etag_in == etag_out) - { - return TEH_RESPONSE_reply_not_modified (rc->connection, - etagp, - &add_response_headers, - NULL); - } - if (NULL == tl) - { - /* 204: empty history */ - resp = MHD_create_response_from_buffer_static (0, - ""); - http_status = MHD_HTTP_NO_CONTENT; - } - else - { - /* 200: regular history */ - json_t *history; - - history = compile_transaction_history (coin_pub, - tl); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - tl = NULL; - if (NULL == history) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - "Failed to compile coin history"); - } - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &h_denom_pub), - TALER_JSON_pack_amount ("balance", - &balance), - GNUNET_JSON_pack_array_steal ("history", - history)); - http_status = MHD_HTTP_OK; - } - add_response_headers (NULL, - resp); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - etagp)); - { - MHD_RESULT ret; - - ret = MHD_queue_response (rc->connection, - http_status, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } -} - - -/* end of taler-exchange-httpd_coins_get.c */ diff --git a/src/exchange/taler-exchange-httpd_coins_get.h b/src/exchange/taler-exchange-httpd_coins_get.h @@ -1,53 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_coins_get.h - * @brief Handle GET /coins/$COIN_PUB requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_COINS_GET_H -#define TALER_EXCHANGE_HTTPD_COINS_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Shutdown reserves-get subsystem. Resumes all - * suspended long-polling clients and cleans up - * data structures. - */ -void -TEH_reserves_get_cleanup (void); - - -/** - * Handle a GET "/coins/$COIN_PUB/history" request. Parses the - * given "coins_pub" in @a args (which should contain the - * EdDSA public key of a reserve) and then respond with the - * transaction history of the coin. - * - * @param rc request context - * @param coin_pub public key of the coin - * @return MHD result code - */ -MHD_RESULT -TEH_handler_coins_get (struct TEH_RequestContext *rc, - const struct TALER_CoinSpendPublicKeyP *coin_pub); - -#endif diff --git a/src/exchange/taler-exchange-httpd_common_deposit.c b/src/exchange/taler-exchange-httpd_common_deposit.c @@ -21,7 +21,7 @@ #include "taler/platform.h" #include "taler-exchange-httpd_common_deposit.h" #include "taler-exchange-httpd.h" -#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_get-keys.h" enum GNUNET_GenericReturnValue diff --git a/src/exchange/taler-exchange-httpd_config.c b/src/exchange/taler-exchange-httpd_config.c @@ -1,77 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2015-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_config.c - * @brief Handle /config requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_json_lib.h> -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_config.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include <jansson.h> - - -MHD_RESULT -TEH_handler_config (struct TEH_RequestContext *rc, - const char *const args[]) -{ - static struct MHD_Response *resp; - - (void) args; - if (NULL == resp) - { - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ( - "wallet_balance_limit_without_kyc", - TALER_KYCLOGIC_get_wallet_thresholds ())), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("shopping_url", - TEH_shopping_url)), - GNUNET_JSON_pack_object_steal ( - "currency_specification", - TALER_JSON_currency_specs_to_json (TEH_cspec)), - GNUNET_JSON_pack_string ( - "currency", - TEH_currency), - GNUNET_JSON_pack_string ( - "name", - "taler-exchange"), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "aml_spa_dialect", - TEH_aml_spa_dialect)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("shopping_url", - TEH_shopping_url)), - GNUNET_JSON_pack_string ( - "implementation", - "urn:net:taler:specs:taler-exchange:c-reference"), - GNUNET_JSON_pack_string ( - "version", - EXCHANGE_PROTOCOL_VERSION)); - } - return MHD_queue_response (rc->connection, - MHD_HTTP_OK, - resp); -} - - -/* end of taler-exchange-httpd_config.c */ diff --git a/src/exchange/taler-exchange-httpd_config.h b/src/exchange/taler-exchange-httpd_config.h @@ -1,58 +0,0 @@ -/* - This file is part of TALER - (C) 2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of EXCHANGEABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_config.h - * @brief headers for /config handler - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_CONFIG_H -#define TALER_EXCHANGE_HTTPD_CONFIG_H -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Taler protocol version in the format CURRENT:REVISION:AGE - * as used by GNU libtool. See - * https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html - * - * Please be very careful when updating and follow - * https://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html#Updating-version-info - * precisely. Note that this version has NOTHING to do with the - * release version, and the format is NOT the same that semantic - * versioning uses either. - * - * When changing this version, you likely want to also update - * #TALER_PROTOCOL_CURRENT and #TALER_PROTOCOL_AGE in - * exchange_api_handle.c! - * - * Returned via both /config and /keys endpoints. - */ -#define EXCHANGE_PROTOCOL_VERSION "34:0:0" - - -/** - * Manages a /config call. - * - * @param rc context of the handler - * @param[in,out] args remaining arguments (ignored) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_config (struct TEH_RequestContext *rc, - const char *const args[]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_contract.c b/src/exchange/taler-exchange-httpd_contract.c @@ -1,99 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_contract.c - * @brief Handle GET /contracts/$C_PUB requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_contract.h" -#include "taler-exchange-httpd_mhd.h" -#include "taler-exchange-httpd_responses.h" - - -MHD_RESULT -TEH_handler_contracts_get (struct TEH_RequestContext *rc, - const char *const args[1]) -{ - struct TALER_ContractDiffiePublicP contract_pub; - struct TALER_PurseContractPublicKeyP purse_pub; - void *econtract; - size_t econtract_size; - enum GNUNET_DB_QueryStatus qs; - struct TALER_PurseContractSignatureP econtract_sig; - MHD_RESULT res; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &contract_pub, - sizeof (contract_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB, - args[0]); - } - - qs = TEH_plugin->select_contract (TEH_plugin->cls, - &contract_pub, - &purse_pub, - &econtract_sig, - &econtract_size, - &econtract); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_contract"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_contract"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_CONTRACTS_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; /* handled below */ - } - res = TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("purse_pub", - &purse_pub), - GNUNET_JSON_pack_data_auto ("econtract_sig", - &econtract_sig), - GNUNET_JSON_pack_data_varsize ("econtract", - econtract, - econtract_size)); - GNUNET_free (econtract); - return res; -} - - -/* end of taler-exchange-httpd_contract.c */ diff --git a/src/exchange/taler-exchange-httpd_contract.h b/src/exchange/taler-exchange-httpd_contract.h @@ -1,44 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_contract.h - * @brief Handle /coins/$COIN_PUB/contract requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_CONTRACT_H -#define TALER_EXCHANGE_HTTPD_CONTRACT_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/contracts/$C_PUB" request. Returns the - * encrypted contract. - * - * @param rc request context - * @param args array of additional options (length: 1, first is the contract_pub) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_contracts_get (struct TEH_RequestContext *rc, - const char *const args[1]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_db.h b/src/exchange/taler-exchange-httpd_db.h @@ -23,7 +23,7 @@ #include <microhttpd.h> #include "taler/taler_exchangedb_plugin.h" -#include "taler-exchange-httpd_metrics.h" +#include "taler-exchange-httpd_get-metrics.h" #include <gnunet/gnunet_mhd_compat.h> diff --git a/src/exchange/taler-exchange-httpd_delete-purses-PURSE_PUB.c b/src/exchange/taler-exchange-httpd_delete-purses-PURSE_PUB.c @@ -0,0 +1,149 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_delete-purses-PURSE_PUB.c + * @brief Handle DELETE /purses/$PID requests; parses the request and + * verifies the signature before handing deletion to the database. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_dbevents.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_delete-purses-PURSE_PUB.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_exchangedb_lib.h" +#include "taler-exchange-httpd_get-keys.h" + + +MHD_RESULT +TEH_handler_purses_delete ( + struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct MHD_Connection *connection = rc->connection; + struct TALER_PurseContractPublicKeyP purse_pub; + struct TALER_PurseContractSignatureP purse_sig; + bool found; + bool decided; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &purse_pub, + sizeof (purse_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, + args[0]); + } + TALER_MHD_parse_request_header_auto_t (connection, + "Taler-Purse-Signature", + &purse_sig); + if (GNUNET_OK != + TALER_wallet_purse_delete_verify (&purse_pub, + &purse_sig)) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/delete request\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID, + NULL); + } + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure"); + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->do_purse_delete (TEH_plugin->cls, + &purse_pub, + &purse_sig, + &decided, + &found); + if (qs <= 0) + { + TALER_LOG_WARNING ( + "Failed to store delete purse information in database\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse delete"); + } + } + if (! found) + { + return TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + } + if (decided) + { + return TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_PURSE_DELETE_ALREADY_DECIDED, + NULL); + } + { + /* Possible minor optimization: integrate notification with + transaction above... */ + struct TALER_PurseEventP rep_deposited = { + .header.size = htons (sizeof (rep_deposited)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), + .purse_pub = purse_pub + }; + struct TALER_PurseEventP rep_merged = { + .header.size = htons (sizeof (rep_merged)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_MERGED), + .purse_pub = purse_pub + }; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying about purse deletion %s\n", + TALER_B2S (&purse_pub)); + TEH_plugin->event_notify (TEH_plugin->cls, + &rep_deposited.header, + NULL, + 0); + TEH_plugin->event_notify (TEH_plugin->cls, + &rep_merged.header, + NULL, + 0); + } + /* success */ + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_purses_delete.c */ diff --git a/src/exchange/taler-exchange-httpd_delete-purses-PURSE_PUB.h b/src/exchange/taler-exchange-httpd_delete-purses-PURSE_PUB.h @@ -0,0 +1,42 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_delete-purses-PURSE_PUB.h + * @brief Handle DELETE /purses/$PID requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_DELETE_PURSES_PURSE_PUB_H +#define TALER_EXCHANGE_HTTPD_DELETE_PURSES_PURSE_PUB_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a DELETE "/purses/$PURSE_PUB" request. + * + * @param rc request details about the request to handle + * @param args argument with the public key of the purse + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_delete ( + struct TEH_RequestContext *rc, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_deposits_get.c b/src/exchange/taler-exchange-httpd_deposits_get.c @@ -1,536 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_deposits_get.c - * @brief Handle wire deposit tracking-related requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_dbevents.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_util.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_deposits_get.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Closure for #handle_wtid_data. - */ -struct DepositWtidContext -{ - - /** - * Kept in a DLL. - */ - struct DepositWtidContext *next; - - /** - * Kept in a DLL. - */ - struct DepositWtidContext *prev; - - /** - * Context for the request we are processing. - */ - struct TEH_RequestContext *rc; - - /** - * Subscription for the database event we are waiting for. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * Hash over the proposal data of the contract for which this deposit is made. - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Hash over the wiring information of the merchant. - */ - struct TALER_MerchantWireHashP h_wire; - - /** - * The Merchant's public key. The deposit inquiry request is to be - * signed by the corresponding private key (using EdDSA). - */ - struct TALER_MerchantPublicKeyP merchant; - - /** - * Public key for KYC operations on the target bank - * account for the wire transfer. All zero if no - * public key is accepted yet. In that case, the - * client should use the @e merchant public key for - * the KYC auth wire transfer. - */ - union TALER_AccountPublicKeyP account_pub; - - /** - * The coin's public key. This is the value that must have been - * signed (blindly) by the Exchange. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Set by #handle_wtid data to the wire transfer ID. - */ - struct TALER_WireTransferIdentifierRawP wtid; - - /** - * Signature by the merchant. - */ - struct TALER_MerchantSignatureP merchant_sig; - - /** - * Set by #handle_wtid data to the coin's contribution to the wire transfer. - */ - struct TALER_Amount coin_contribution; - - /** - * Set by #handle_wtid data to the fee charged to the coin. - */ - struct TALER_Amount coin_fee; - - /** - * Set by #handle_wtid data to the wire transfer execution time. - */ - struct GNUNET_TIME_Timestamp execution_time; - - /** - * Timeout of the request, for long-polling. - */ - struct GNUNET_TIME_Absolute timeout; - - /** - * Set by #handle_wtid to the coin contribution to the transaction - * (that is, @e coin_contribution minus @e coin_fee). - */ - struct TALER_Amount coin_delta; - - /** - * KYC status information for the receiving account. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - - /** - * #GNUNET_YES if we were suspended, #GNUNET_SYSERR - * if we were woken up due to shutdown. - */ - enum GNUNET_GenericReturnValue suspended; - - /** - * What do we long-poll for? Defaults to - * #TALER_DGLPT_OK if not given. - */ - enum TALER_DepositGetLongPollTarget lpt; -}; - - -/** - * Head of DLL of suspended requests. - */ -static struct DepositWtidContext *dwc_head; - -/** - * Tail of DLL of suspended requests. - */ -static struct DepositWtidContext *dwc_tail; - - -void -TEH_deposits_get_cleanup () -{ - struct DepositWtidContext *n; - - for (struct DepositWtidContext *ctx = dwc_head; - NULL != ctx; - ctx = n) - { - n = ctx->next; - GNUNET_assert (GNUNET_YES == ctx->suspended); - ctx->suspended = GNUNET_SYSERR; - MHD_resume_connection (ctx->rc->connection); - GNUNET_CONTAINER_DLL_remove (dwc_head, - dwc_tail, - ctx); - } -} - - -/** - * A merchant asked for details about a deposit. Provide - * them. Generates the 200 reply. - * - * @param ctx details to respond with - * @return MHD result code - */ -static MHD_RESULT -reply_deposit_details ( - const struct DepositWtidContext *ctx) -{ - struct MHD_Connection *connection = ctx->rc->connection; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec; - - if (TALER_EC_NONE != - (ec = TALER_exchange_online_confirm_wire_sign ( - &TEH_keys_exchange_sign_, - &ctx->h_wire, - &ctx->h_contract_terms, - &ctx->wtid, - &ctx->coin_pub, - ctx->execution_time, - &ctx->coin_delta, - &pub, - &sig))) - { - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("wtid", - &ctx->wtid), - GNUNET_JSON_pack_timestamp ("execution_time", - ctx->execution_time), - TALER_JSON_pack_amount ("coin_contribution", - &ctx->coin_delta), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); -} - - -/** - * Function called on events received from Postgres. - * Wakes up long pollers. - * - * @param cls the `struct DepositWtidContext *` - * @param extra additional event data provided - * @param extra_size number of bytes in @a extra - */ -static void -db_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - struct DepositWtidContext *ctx = cls; - struct GNUNET_AsyncScopeSave old_scope; - - (void) extra; - (void) extra_size; - if (GNUNET_YES != ctx->suspended) - return; /* might get multiple wake-up events */ - GNUNET_CONTAINER_DLL_remove (dwc_head, - dwc_tail, - ctx); - GNUNET_async_scope_enter (&ctx->rc->async_scope_id, - &old_scope); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming request handling\n"); - TEH_check_invariants (); - ctx->suspended = GNUNET_NO; - MHD_resume_connection (ctx->rc->connection); - TALER_MHD_daemon_trigger (); - TEH_check_invariants (); - GNUNET_async_scope_restore (&old_scope); -} - - -/** - * Lookup and return the wire transfer identifier. - * - * @param ctx context of the signed request to execute - * @return MHD result code - */ -static MHD_RESULT -handle_track_transaction_request ( - struct DepositWtidContext *ctx) -{ - struct MHD_Connection *connection = ctx->rc->connection; - enum GNUNET_DB_QueryStatus qs; - bool pending; - struct TALER_Amount fee; - - qs = TEH_plugin->lookup_transfer_by_deposit ( - TEH_plugin->cls, - &ctx->h_contract_terms, - &ctx->h_wire, - &ctx->coin_pub, - &ctx->merchant, - &pending, - &ctx->wtid, - &ctx->execution_time, - &ctx->coin_contribution, - &fee, - &ctx->kyc, - &ctx->account_pub); - if (0 > qs) - { - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_transfer_by_deposit"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_DEPOSITS_GET_NOT_FOUND, - NULL); - } - - if (0 > - TALER_amount_subtract (&ctx->coin_delta, - &ctx->coin_contribution, - &fee)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "wire fees exceed aggregate in database"); - } - if (pending) - { - if (GNUNET_TIME_absolute_is_future (ctx->timeout)) - { - bool do_suspend = false; - switch (ctx->lpt) - { - case TALER_DGLPT_NONE: - break; - case TALER_DGLPT_KYC_REQUIRED_OR_OK: - do_suspend = ctx->kyc.ok; - break; - case TALER_DGLPT_OK: - do_suspend = true; - break; - } - if (do_suspend) - { - GNUNET_assert (GNUNET_NO == ctx->suspended); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending request handling\n"); - GNUNET_CONTAINER_DLL_insert (dwc_head, - dwc_tail, - ctx); - ctx->suspended = GNUNET_YES; - MHD_suspend_connection (connection); - return MHD_YES; - } - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC required with row %llu\n", - (unsigned long long) ctx->kyc.requirement_row); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_allow_null ( - (0 == ctx->kyc.requirement_row) - ? GNUNET_JSON_pack_string ("requirement_row", - NULL) - : GNUNET_JSON_pack_uint64 ("requirement_row", - ctx->kyc.requirement_row)), - GNUNET_JSON_pack_allow_null ( - (GNUNET_is_zero (&ctx->account_pub)) - ? GNUNET_JSON_pack_string ("account_pub", - NULL) - : GNUNET_JSON_pack_data_auto ("account_pub", - &ctx->account_pub)), - GNUNET_JSON_pack_bool ("kyc_ok", - ctx->kyc.ok), - GNUNET_JSON_pack_timestamp ("execution_time", - ctx->execution_time)); - } - return reply_deposit_details (ctx); -} - - -/** - * Function called to clean up a context. - * - * @param rc request context with data to clean up - */ -static void -dwc_cleaner (struct TEH_RequestContext *rc) -{ - struct DepositWtidContext *ctx = rc->rh_ctx; - - GNUNET_assert (GNUNET_YES != ctx->suspended); - if (NULL != ctx->eh) - { - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - ctx->eh); - ctx->eh = NULL; - } - GNUNET_free (ctx); -} - - -MHD_RESULT -TEH_handler_deposits_get (struct TEH_RequestContext *rc, - const char *const args[4]) -{ - struct DepositWtidContext *ctx = rc->rh_ctx; - - if (NULL == ctx) - { - ctx = GNUNET_new (struct DepositWtidContext); - ctx->rc = rc; - ctx->lpt = TALER_DGLPT_OK; /* default */ - rc->rh_ctx = ctx; - rc->rh_cleaner = &dwc_cleaner; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &ctx->h_wire, - sizeof (ctx->h_wire))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE, - args[0]); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[1], - strlen (args[1]), - &ctx->merchant, - sizeof (ctx->merchant))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB, - args[1]); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[2], - strlen (args[2]), - &ctx->h_contract_terms, - sizeof (ctx->h_contract_terms))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS, - args[2]); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[3], - strlen (args[3]), - &ctx->coin_pub, - sizeof (ctx->coin_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB, - args[3]); - } - TALER_MHD_parse_request_arg_auto_t (rc->connection, - "merchant_sig", - &ctx->merchant_sig); - TALER_MHD_parse_request_timeout (rc->connection, - &ctx->timeout); - { - uint64_t num = 0; - int val; - - TALER_MHD_parse_request_number (rc->connection, - "lpt", - &num); - val = (int) num; - if ( (val < 0) || - (val > TALER_DGLPT_MAX) ) - { - /* Protocol violation, but we can be graceful and - just ignore the long polling! */ - GNUNET_break_op (0); - val = TALER_DGLPT_NONE; - } - ctx->lpt = (enum TALER_DepositGetLongPollTarget) val; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Long polling for target %d with timeout %s\n", - val, - GNUNET_TIME_relative2s ( - GNUNET_TIME_absolute_get_remaining ( - ctx->timeout), - true)); - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - { - if (GNUNET_OK != - TALER_merchant_deposit_verify (&ctx->merchant, - &ctx->coin_pub, - &ctx->h_contract_terms, - &ctx->h_wire, - &ctx->merchant_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID, - NULL); - } - } - if ( (GNUNET_TIME_absolute_is_future (ctx->timeout)) && - (TALER_DGLPT_NONE != ctx->lpt) ) - { - struct TALER_CoinDepositEventP rep = { - .header.size = htons (sizeof (rep)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_DEPOSIT_STATUS_CHANGED), - .merchant_pub = ctx->merchant - }; - - ctx->eh = TEH_plugin->event_listen ( - TEH_plugin->cls, - GNUNET_TIME_absolute_get_remaining (ctx->timeout), - &rep.header, - &db_event_cb, - ctx); - GNUNET_break (NULL != ctx->eh); - } - } - - return handle_track_transaction_request (ctx); -} - - -/* end of taler-exchange-httpd_deposits_get.c */ diff --git a/src/exchange/taler-exchange-httpd_deposits_get.h b/src/exchange/taler-exchange-httpd_deposits_get.h @@ -1,50 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2017, 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_deposits_get.h - * @brief Handle wire transfer tracking-related requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_DEPOSITS_GET_H -#define TALER_EXCHANGE_HTTPD_DEPOSITS_GET_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Resume long pollers on GET /deposits. - */ -void -TEH_deposits_get_cleanup (void); - - -/** - * Handle a "/deposits/$H_WIRE/$MERCHANT_PUB/$H_CONTRACT_TERMS/$COIN_PUB" - * request. - * - * @param rc request context - * @param args array of additional options (length: 4, contains: - * h_wire, merchant_pub, h_contract_terms and coin_pub) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_deposits_get (struct TEH_RequestContext *rc, - const char *const args[4]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_extensions.c b/src/exchange/taler-exchange-httpd_extensions.c @@ -20,7 +20,7 @@ #include "taler/platform.h" #include <gnunet/gnunet_json_lib.h> #include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_get-keys.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_extensions.h" #include "taler/taler_extensions_policy.h" diff --git a/src/exchange/taler-exchange-httpd_get-SPA.c b/src/exchange/taler-exchange-httpd_get-SPA.c @@ -0,0 +1,128 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2023, 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of EXCHANGEABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-SPA.c + * @brief logic to load single page apps (/) + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "taler/taler_util.h" +#include "taler/taler_mhd_lib.h" +#include <gnunet/gnunet_mhd_compat.h> +#include "taler-exchange-httpd.h" +#include "taler-exchange-httpd_get-SPA.h" + + +/** + * Resources of the AML SPA. + */ +static struct TALER_MHD_Spa *aml_spa; + +/** + * Resources of the KYC SPA. + */ +static struct TALER_MHD_Spa *kyc_spa; + + +MHD_RESULT +TEH_handler_aml_spa (struct TEH_RequestContext *rc, + const char *const args[]) +{ + const char *path = args[0]; + + return TALER_MHD_spa_handler (aml_spa, + rc->connection, + path); +} + + +MHD_RESULT +TEH_handler_kyc_spa (struct TEH_RequestContext *rc, + const char *const args[]) +{ + const char *path = args[0]; + struct TALER_AccountAccessTokenP tok; + + if (NULL == path) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT, + "no account access token specified"); + } + if (GNUNET_OK == + GNUNET_STRINGS_string_to_data (path, + strlen (path), + &tok, + sizeof (tok))) + { + /* The access token is used internally by the SPA, + we simply map all access tokens to "index.html" */ + path = "index.html"; + } + return TALER_MHD_spa_handler (kyc_spa, + rc->connection, + path); +} + + +enum GNUNET_GenericReturnValue +TEH_spa_init () +{ + aml_spa = TALER_MHD_spa_load (TALER_EXCHANGE_project_data (), + "aml-spa/"); + if (NULL == aml_spa) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + kyc_spa = TALER_MHD_spa_load (TALER_EXCHANGE_project_data (), + "kyc-spa/"); + if (NULL == kyc_spa) + { + GNUNET_break (0); + TALER_MHD_spa_free (aml_spa); + aml_spa = NULL; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/* Suppresses warning */ +void __attribute__ ((destructor)) +get_spa_fini (void); + +/** + * Nicely shut down. + */ +void __attribute__ ((destructor)) +get_spa_fini (void) +{ + if (NULL != kyc_spa) + { + TALER_MHD_spa_free (kyc_spa); + kyc_spa = NULL; + } + if (NULL != aml_spa) + { + TALER_MHD_spa_free (aml_spa); + aml_spa = NULL; + } +} diff --git a/src/exchange/taler-exchange-httpd_get-SPA.h b/src/exchange/taler-exchange-httpd_get-SPA.h @@ -0,0 +1,63 @@ +/* + 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 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 EXCHANGEABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-SPA.h + * @brief logic to preload and serve static files + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_SPA_H +#define TALER_EXCHANGE_HTTPD_GET_SPA_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Return our single-page-app user interface + * for AML staff (see contrib/wallet-core/). + * + * @param rc context of the handler + * @param[in,out] args remaining arguments (ignored) + * @return #MHD_YES on success (reply queued), #MHD_NO on error (close connection) + */ +MHD_RESULT +TEH_handler_aml_spa (struct TEH_RequestContext *rc, + const char *const args[]); + + +/** + * Return our single-page-app user interface + * for the KYC process (see contrib/wallet-core/). + * + * @param rc context of the handler + * @param[in,out] args remaining arguments (ignored) + * @return #MHD_YES on success (reply queued), #MHD_NO on error (close connection) + */ +MHD_RESULT +TEH_handler_kyc_spa (struct TEH_RequestContext *rc, + const char *const args[]); + + +/** + * Preload and compress SPA files. + * + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TEH_spa_init (void); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-TERMS.c b/src/exchange/taler-exchange-httpd_get-TERMS.c @@ -0,0 +1,82 @@ +/* + This file is part of TALER + Copyright (C) 2019, 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-TERMS.c + * @brief Handle /terms requests to return the terms of service + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-TERMS.h" + +/** + * Our terms of service. + */ +static struct TALER_MHD_Legal *tos; + + +/** + * Our privacy policy. + */ +static struct TALER_MHD_Legal *pp; + + +MHD_RESULT +TEH_handler_terms (struct TEH_RequestContext *rc, + const char *const args[]) +{ + (void) args; + return TALER_MHD_reply_legal (rc->connection, + tos); +} + + +MHD_RESULT +TEH_handler_privacy (struct TEH_RequestContext *rc, + const char *const args[]) +{ + (void) args; + return TALER_MHD_reply_legal (rc->connection, + pp); +} + + +void +TEH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + tos = TALER_MHD_legal_load (cfg, + "exchange", + "TERMS_DIR", + "TERMS_ETAG"); + if (NULL == tos) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Terms of service not configured\n"); + pp = TALER_MHD_legal_load (cfg, + "exchange", + "PRIVACY_DIR", + "PRIVACY_ETAG"); + if (NULL == pp) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Privacy policy not configured\n"); +} + + +/* end of taler-exchange-httpd_terms.c */ diff --git a/src/exchange/taler-exchange-httpd_get-TERMS.h b/src/exchange/taler-exchange-httpd_get-TERMS.h @@ -0,0 +1,65 @@ +/* + This file is part of TALER + Copyright (C) 2019, 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-TERMS.h + * @brief Handle /terms requests to return the terms of service + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_TERMS_H +#define TALER_EXCHANGE_HTTPD_GET_TERMS_H +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Handle a "/terms" request. + * + * @param rc request context + * @param args array of additional options (must be empty for this function) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_terms (struct TEH_RequestContext *rc, + const char *const args[]); + + +/** + * Handle a "/privacy" request. + * + * @param rc request context + * @param args array of additional options (must be empty for this function) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_privacy (struct TEH_RequestContext *rc, + const char *const args[]); + + +/** + * Load our terms of service as per configuration. + * + * @param cfg configuration to process + */ +void +TEH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c @@ -0,0 +1,486 @@ +/* + This file is part of TALER + Copyright (C) 2024, 2025, 2026 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c + * @brief Return summary information about accounts + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.h" + + +/** + * Maximum number of records we return in one go. Must be + * small enough to ensure that XML/CSV encoded results stay + * below the 16MB limit of GNUNET_malloc(). + */ +#define MAX_RECORDS (1024 * 64) + +#define CSV_HEADER \ + "File number,Customer,Comments,Risky,Acquisition date,Exit date\r\n" +#define CSV_FOOTER "\r\n" + +#define XML_HEADER "<?xml version=\"1.0\" encoding=\"UTF 8\"?>" \ + "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \ + " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \ + " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \ + " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \ + " xmlns:o=\"urn:schemas-microsoft-com:office:office\"" \ + " xmlns:x=\"urn:schemas-microsoft-com:office:excel\"" \ + " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \ + "<Styles><Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd\"/></Style></Styles>\n" \ + "<Worksheet ss:Name=\"Accounts\">\n" \ + "<Table>\n" \ + "<Row>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">File number</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Customer</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Comments</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Increased risk business relationship</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Acquisition date</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Exit date</Data></Cell>\n" \ + "</Row>\n" +#define XML_FOOTER "</Table></Worksheet></Workbook>" + +/** + * Closure for the record_cb(). + */ +struct ResponseContext +{ + /** + * Format of the response we are to generate. + */ + enum + { + RCF_JSON, + RCF_XML, + RCF_CSV + } format; + + /** + * Where we store the response data. + */ + union + { + /** + * If @e format is #RCF_JSON. + */ + json_t *json; + + /** + * If @e format is #RCF_XML. + */ + struct GNUNET_Buffer xml; + + /** + * If @e format is #RCF_CSV. + */ + struct GNUNET_Buffer csv; + + } details; +}; + + +/** + * Free resources from @a rc + * + * @param[in] rc context to clean up + */ +static void +free_rc (struct ResponseContext *rc) +{ + switch (rc->format) + { + case RCF_JSON: + json_decref (rc->details.json); + break; + case RCF_XML: + GNUNET_buffer_clear (&rc->details.xml); + break; + case RCF_CSV: + GNUNET_buffer_clear (&rc->details.csv); + break; + } +} + + +/** + * Return account summary information. + * + * @param cls closure + * @param row_id current row in AML status table + * @param h_payto account for which the attribute data is stored + * @param open_time when was the account opened formally, + * GNUNET_TIME_UNIT_FOREVER_TS if it was never opened + * @param close_time when was the account formally closed, + * GNUNET_TIME_UNIT_ZERO_TS if it was never closed + * @param comments comments on the account + * @param high_risk is this a high-risk business relationship + * @param to_investigate TRUE if this account should be investigated + * @param payto the payto URI of the account + */ +static void +record_cb ( + void *cls, + uint64_t row_id, + const struct TALER_NormalizedPaytoHashP *h_payto, + struct GNUNET_TIME_Timestamp open_time, + struct GNUNET_TIME_Timestamp close_time, + const char *comments, + bool high_risk, + bool to_investigate, + struct TALER_FullPayto payto) +{ + struct ResponseContext *rc = cls; + + if ( (NULL == comments) && + (GNUNET_TIME_absolute_is_never (open_time.abs_time)) ) + comments = "transacted amounts below limits that trigger account opening"; + if (NULL == comments) + comments = ""; + switch (rc->format) + { + case RCF_JSON: + GNUNET_assert ( + 0 == + json_array_append_new ( + rc->details.json, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + TALER_JSON_pack_full_payto ("full_payto", + payto), + GNUNET_JSON_pack_bool ("high_risk", + high_risk), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("comments", + comments)), + GNUNET_JSON_pack_int64 ("rowid", + row_id), + GNUNET_JSON_pack_timestamp ("open_time", + open_time), + GNUNET_JSON_pack_timestamp ("close_time", + close_time), + GNUNET_JSON_pack_bool ("to_investigate", + to_investigate) + ))); + return; + case RCF_XML: + { + char *epayto; + char *ecomments = NULL; + char opentime_s[128]; + char closetime_s[128]; + const struct tm *tm; + time_t tt; + + epayto = TALER_escape_xml (payto.full_payto); + if ( (NULL == comments) && + (GNUNET_TIME_absolute_is_never (open_time.abs_time)) ) + comments = + "transacted amounts below limits that trigger account opening"; + ecomments = TALER_escape_xml (comments); + tt = (time_t) GNUNET_TIME_timestamp_to_s (open_time); + tm = gmtime (&tt); + strftime (opentime_s, + sizeof (opentime_s), + "%Y-%m-%dT%H:%M:%S", + tm); + tt = (time_t) GNUNET_TIME_timestamp_to_s (close_time); + tm = gmtime (&tt); + strftime (closetime_s, + sizeof (closetime_s), + "%Y-%m-%dT%H:%M:%S", + tm); + GNUNET_buffer_write_fstr ( + &rc->details.xml, + "<Row>" + "<Cell><Data ss:Type=\"Number\">%llu</Data></Cell>" + "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>" + "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>" + "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>" + "</Row>\n", + (unsigned long long) row_id, + epayto, + NULL == ecomments + ? "" + : ecomments, + high_risk ? "TRUE" : "FALSE", + high_risk ? "1" : "0", + GNUNET_TIME_absolute_is_never (open_time.abs_time) + ? "String" + : "DateTime", + GNUNET_TIME_absolute_is_never (open_time.abs_time) + ? "never" + : opentime_s, + GNUNET_TIME_absolute_is_never (close_time.abs_time) + ? "String" + : "DateTime", + GNUNET_TIME_absolute_is_never (close_time.abs_time) + ? "never" + : closetime_s); + GNUNET_free (ecomments); + GNUNET_free (epayto); + break; + } /* end case RCF_XML */ + case RCF_CSV: + { + char *ecomments; + char otbuf[64]; + char ctbuf[64]; + size_t len = strlen (comments); + size_t wpos = 0; + + GNUNET_snprintf (otbuf, + sizeof (otbuf), + "%s", + GNUNET_TIME_timestamp2s (open_time)); + GNUNET_snprintf (ctbuf, + sizeof (ctbuf), + "%s", + GNUNET_TIME_timestamp2s (close_time)); + /* Escape 'comments' to double '"' as per RFC 4180, 2.7. */ + ecomments = GNUNET_malloc (2 * len + 1); + for (size_t off = 0; off<len; off++) + { + if ('"' == comments[off]) + ecomments[wpos++] = '"'; + ecomments[wpos++] = comments[off]; + } + GNUNET_buffer_write_fstr (&rc->details.csv, + "%llu,\"%s\",\"%s\",%s,%s,%s\r\n", + (unsigned long long) row_id, + payto.full_payto, + ecomments, + high_risk ? "X":" ", + GNUNET_TIME_absolute_is_never (open_time. + abs_time) + ? "-" + : otbuf, + GNUNET_TIME_absolute_is_never (close_time. + abs_time) + ? "-" + : ctbuf); + GNUNET_free (ecomments); + break; + } /* end case RCF_CSV */ + } /* end switch */ +} + + +MHD_RESULT +TEH_handler_aml_accounts_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + struct ResponseContext rctx; + int64_t limit = -20; + uint64_t offset; + enum TALER_EXCHANGE_YesNoAll open_filter; + enum TALER_EXCHANGE_YesNoAll high_risk_filter; + enum TALER_EXCHANGE_YesNoAll investigation_filter; + + memset (&rctx, + 0, + sizeof (rctx)); + if (NULL != args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[0]); + } + TALER_MHD_parse_request_snumber (rc->connection, + "limit", + &limit); + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; + TALER_MHD_parse_request_number (rc->connection, + "offset", + &offset); + if (offset > INT64_MAX) + { + GNUNET_break_op (0); /* broken client */ + offset = INT64_MAX; + } + TALER_MHD_parse_request_yna (rc->connection, + "open", + TALER_EXCHANGE_YNA_ALL, + &open_filter); + TALER_MHD_parse_request_yna (rc->connection, + "investigation", + TALER_EXCHANGE_YNA_ALL, + &investigation_filter); + TALER_MHD_parse_request_yna (rc->connection, + "high_risk", + TALER_EXCHANGE_YNA_ALL, + &high_risk_filter); + { + const char *mime; + + mime = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "application/json"; + if (0 == strcmp (mime, + "application/json")) + { + rctx.format = RCF_JSON; + rctx.details.json = json_array (); + GNUNET_assert (NULL != rctx.details.json); + } + else if (0 == strcmp (mime, + "application/vnd.ms-excel")) + { + rctx.format = RCF_XML; + GNUNET_buffer_write_str (&rctx.details.xml, + XML_HEADER); + } + else if (0 == strcmp (mime, + "text/csv")) + { + rctx.format = RCF_CSV; + GNUNET_buffer_write_str (&rctx.details.csv, + CSV_HEADER); + } + else + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_ACCEPTABLE, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + mime); + } + } + + { + enum GNUNET_DB_QueryStatus qs; + + if (limit > MAX_RECORDS) + limit = MAX_RECORDS; + if (limit < -MAX_RECORDS) + limit = -MAX_RECORDS; + qs = TEH_plugin->select_kyc_accounts ( + TEH_plugin->cls, + investigation_filter, + open_filter, + high_risk_filter, + offset, + limit, + &record_cb, + &rctx); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + free_rc (&rctx); + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_kyx_accounts"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + free_rc (&rctx); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } /* end switch (qs) */ + + switch (rctx.format) + { + case RCF_JSON: + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("accounts", + rctx.details.json)); + case RCF_XML: + { + struct MHD_Response *resp; + MHD_RESULT mret; + + GNUNET_buffer_write_str (&rctx.details.xml, + XML_FOOTER); + /* FIXME: add support for compression */ + resp = MHD_create_response_from_buffer (rctx.details.xml.position, + rctx.details.xml.mem, + MHD_RESPMEM_MUST_FREE); + TALER_MHD_add_global_headers (resp, + false); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "application/vnd.ms-excel")); + mret = MHD_queue_response (rc->connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return mret; + } + case RCF_CSV: + { + struct MHD_Response *resp; + MHD_RESULT mret; + + GNUNET_buffer_write_str (&rctx.details.csv, + CSV_FOOTER); + /* FIXME: add support for compression */ + resp = MHD_create_response_from_buffer (rctx.details.csv.position, + rctx.details.csv.mem, + MHD_RESPMEM_MUST_FREE); + TALER_MHD_add_global_headers (resp, + false); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/csv")); + mret = MHD_queue_response (rc->connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return mret; + } + } /* end switch (rctx.format) */ + } + GNUNET_break (0); + return MHD_NO; +} + + +/* end of taler-exchange-httpd_aml-accounts_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.h b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.h + * @brief Handle GET /aml/$OFFICER_PUB/accounts requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_ACCOUNTS_H +#define TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_ACCOUNTS_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/aml/$OFFICER_PUB/accounts" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching decisions. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be the state) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_accounts_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.c @@ -0,0 +1,815 @@ +/* + This file is part of TALER + Copyright (C) 2024, 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.c + * @brief Return summary information about KYC attributes + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler/taler_exchangedb_plugin.h" +#include \ + "taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.h" +#include "taler-exchange-httpd_get-metrics.h" + +/** + * Maximum number of records we return in one request. + */ +#define MAX_RECORDS 1024 + + +/** + * Closure for the detail_cb(). + */ +struct ResponseContext +{ + /** + * Format of the response we are to generate. + */ + enum + { + RCF_JSON, + RCF_PDF + } format; + + /** + * Stored in a DLL while suspended. + */ + struct ResponseContext *next; + + /** + * Stored in a DLL while suspended. + */ + struct ResponseContext *prev; + + /** + * Context for this request. + */ + struct TEH_RequestContext *rc; + + /** + * Async context used to run Typst. + */ + struct TALER_MHD_TypstContext *tc; + + /** + * Response to return. + */ + struct MHD_Response *response; + + /** + * HTTP status to use with @e response. + */ + unsigned int http_status; + + /** + * True if this is for a wallet. + */ + bool is_wallet; + + /** + * Where we store the response data. + */ + union + { + /** + * If @e format is #RCF_JSON. + */ + json_t *json; + + /** + * If @e format is #RCF_PDF. + */ + struct + { + + /** + * Typst forms to compile. + */ + struct TALER_MHD_TypstDocument docs[MAX_RECORDS]; + + /** + * JSON data for each Typst form. + */ + json_t *jdata[MAX_RECORDS]; + + /** + * Next write offset into @e docs and @e jdata. + */ + size_t off; + + /** + * Global attributes we need to inject in to each @e jdata. + */ + json_t *global_attrs; + + } pdf; + + } details; +}; + + +/** + * DLL of requests awaiting Typst. + */ +static struct ResponseContext *rctx_head; + +/** + * DLL of requests awaiting Typst. + */ +static struct ResponseContext *rctx_tail; + + +void +TEH_handler_aml_attributes_get_cleanup () +{ + struct ResponseContext *rctx; + + while (NULL != (rctx = rctx_head)) + { + GNUNET_CONTAINER_DLL_remove (rctx_head, + rctx_tail, + rctx); + MHD_resume_connection (rctx->rc->connection); + } +} + + +/** + * Free resources from @a rc + * + * @param[in] rc context to clean up + */ +static void +free_rc (struct TEH_RequestContext *rc) +{ + struct ResponseContext *rctx = rc->rh_ctx; + + if (NULL != rctx->tc) + { + TALER_MHD_typst_cancel (rctx->tc); + rctx->tc = NULL; + } + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + switch (rctx->format) + { + case RCF_JSON: + json_decref (rctx->details.json); + break; + case RCF_PDF: + for (size_t i = 0; i<rctx->details.pdf.off; i++) + { + json_decref (rctx->details.pdf.jdata[i]); + rctx->details.pdf.jdata[i] = NULL; + } + json_decref (rctx->details.pdf.global_attrs); + rctx->details.pdf.global_attrs = NULL; + break; + } + GNUNET_free (rctx); +} + + +/** + * Dump attachment @a attach into @a rc. + * + * @param[in,out] rc response context to update + * @param attach attachment to dump + * @return true on success + */ +static bool +dump_attachment (struct ResponseContext *rc, + const json_t *attach) +{ + if (! json_is_string (attach)) + return false; + if (rc->details.pdf.off >= MAX_RECORDS) + { + GNUNET_break (0); + return false; + } + rc->details.pdf.jdata[rc->details.pdf.off] + = json_incref ((json_t *) attach); + rc->details.pdf.docs[rc->details.pdf.off].form_name + = NULL; /* inline PDF */ + rc->details.pdf.docs[rc->details.pdf.off].form_version + = NULL; /* none */ + rc->details.pdf.docs[rc->details.pdf.off].data + = rc->details.pdf.jdata[rc->details.pdf.off]; + rc->details.pdf.off++; + return true; +} + + +/** + * Recursively scan @a attrs for ".FILE" members and dump those + * attachments into @a rc. + * + * @param[in,out] rc response context to update + * @param attrs attributes to scan recursively + * @return true on success + */ +static bool +dump_attachments (struct ResponseContext *rc, + const json_t *attrs) +{ + bool ret = true; + + if (json_is_array (attrs)) + { + const json_t *e; + size_t i; + + json_array_foreach ((json_t *) attrs, i, e) + if (! dump_attachments (rc, + e)) + ret = false; + } + if (json_is_object (attrs)) + { + const json_t *e; + const char *k; + + json_object_foreach ((json_t *) attrs, k, e) + { + if (0 == strcmp (k, + "CONTENTS")) + { + const char *v; + + /* Make sure this is the supported encoding */ + v = json_string_value (json_object_get (attrs, + "ENCODING")); + if (NULL == v) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "`CONTENTS' attribute found without `ENCODING', skipping dumping attachment\n"); + continue; /* maybe not a file!? */ + } + if (0 != strcmp (v, + "base64")) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Attachment with unsupported encoding `%s' encountered, skipping attachment in PDF generation\n", + v); + continue; + } + v = json_string_value (json_object_get (attrs, + "MIME_TYPE")); + if (NULL == v) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "`CONTENTS' attribute found without `MIME_TYPE', skipping dumping attachment\n"); + continue; + } + if (0 != strcmp (v, + "application/pdf")) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Attachment with unsupported mime type `%s' encountered, skipping attachment in PDF generation\n", + v); + continue; + } + /* Filename is optional, only log it */ + v = json_string_value (json_object_get (attrs, + "FILENAME")); + if (NULL != v) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Dumping attachment `%s'\n", + v); + } + if (! dump_attachment (rc, + e)) + ret = false; + } + else if (! dump_attachments (rc, + e)) + ret = false; + } + } + return ret; +} + + +/** + * Return AML account attributes. + * + * @param cls closure + * @param row_id current row in kyc_attributes table + * @param collection_time when were the attributes collected + * @param by_aml_officer was the attribute set filed by the AML officer + * @param staff_name name of the officer, NULL if not @a by_aml_officer + * @param enc_attributes_size length of @a enc_attributes + * @param enc_attributes the encrypted collected attributes + */ +static void +detail_cb ( + void *cls, + uint64_t row_id, + struct GNUNET_TIME_Timestamp collection_time, + bool by_aml_officer, + const char *staff_name, + size_t enc_attributes_size, + const void *enc_attributes) +{ + static char *datadir = NULL; + struct ResponseContext *rc = cls; + json_t *attrs; + const char *form_id; + + if (NULL == datadir) + { + datadir = GNUNET_OS_installation_get_path ( + TALER_EXCHANGE_project_data (), + GNUNET_OS_IPK_DATADIR); + } + attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key, + enc_attributes, + enc_attributes_size); + if (NULL == attrs) + { + GNUNET_break (0); + return; + } + form_id = json_string_value (json_object_get (attrs, + "FORM_ID")); + if (NULL == form_id) + { + GNUNET_break (0); + return; + } + switch (rc->format) + { + case RCF_JSON: + GNUNET_assert ( + 0 == + json_array_append_new ( + rc->details.json, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("rowid", + row_id), + GNUNET_JSON_pack_bool ("by_aml_officer", + by_aml_officer), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("attributes", + attrs)), + GNUNET_JSON_pack_timestamp ("collection_time", + collection_time) + ))); + break; + case RCF_PDF: + if (rc->details.pdf.off >= MAX_RECORDS) + { + GNUNET_break (0); + return; + } + GNUNET_assert (0 == + json_object_set_new (attrs, + "DATADIR", + json_string (datadir))); + GNUNET_assert (0 == + json_object_set_new (attrs, + "BY_AML_OFFICER", + json_boolean (by_aml_officer))); + GNUNET_assert (0 == + json_object_set_new (attrs, + "FILING_DATE", + json_string ( + GNUNET_STRINGS_timestamp_to_string ( + collection_time)))); + if (by_aml_officer) + GNUNET_assert (0 == + json_object_set_new (attrs, + "AML_STAFF_NAME", + json_string (staff_name))); + { + char *have; + + GNUNET_asprintf (&have, + "HAVE_%s", + form_id); + GNUNET_assert (0 == + json_object_set_new (rc->details.pdf.global_attrs, + have, + json_true ())); + GNUNET_free (have); + } + rc->details.pdf.jdata[rc->details.pdf.off] + = attrs; + rc->details.pdf.docs[rc->details.pdf.off].form_name + = form_id; + rc->details.pdf.docs[rc->details.pdf.off].form_version + = NULL; /* not yet supported! */ + rc->details.pdf.docs[rc->details.pdf.off].data + = rc->details.pdf.jdata[rc->details.pdf.off]; + rc->details.pdf.off++; + if (! dump_attachments (rc, + attrs)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to dump some attachment!\n"); + } + break; + } +} + + +/** + * Function called with the result of a #TALER_MHD_typst() operation. + * + * @param cls closure + * @param tr result of the operation + */ +static void +pdf_cb (void *cls, + const struct TALER_MHD_TypstResponse *tr) +{ + struct ResponseContext *rctx = cls; + + rctx->tc = NULL; + GNUNET_CONTAINER_DLL_remove (rctx_head, + rctx_tail, + rctx); + MHD_resume_connection (rctx->rc->connection); + TALER_MHD_daemon_trigger (); + if (TALER_EC_NONE != tr->ec) + { + rctx->http_status + = TALER_ErrorCode_get_http_status (tr->ec); + rctx->response + = TALER_MHD_make_error (tr->ec, + tr->details.hint); + return; + } + rctx->http_status + = MHD_HTTP_OK; + rctx->response + = TALER_MHD_response_from_pdf_file (tr->details.filename); +} + + +/** + * Function called with the latest AML decision on an + * account. Used to build the cover page. + * + * @param cls a `struct ResponseContext *` + * @param outcome_serial_id row ID of the decision + * @param decision_time when was the decision taken + * @param justification what was the given justification + * @param decider_pub which key signed the decision + * @param jproperties what are the new account properties + * @param jnew_rules what are the new account rules + * @param to_investigate should AML staff investigate + * after the decision + * @param is_active is this the active decision + */ +static void +build_cover_page ( + void *cls, + uint64_t outcome_serial_id, + struct GNUNET_TIME_Timestamp decision_time, + const char *justification, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const json_t *jproperties, + const json_t *jnew_rules, + bool to_investigate, + bool is_active) +{ + struct ResponseContext *rc = cls; + json_t *cover; + json_t *defr = NULL; + + if (NULL == jnew_rules) + { + defr = TALER_KYCLOGIC_get_default_legi_rules (rc->is_wallet); + jnew_rules = defr; + } + cover = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("properties", + (json_t *) jproperties)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("rules", + (json_t *) jnew_rules)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("last_justification", + justification)), + GNUNET_JSON_pack_bool ("is_active", + is_active), + GNUNET_JSON_pack_bool ("to_investigate", + to_investigate)); + if (NULL != defr) + json_decref (defr); + /* first page, so we really cannot have hit the maximum yet */ + GNUNET_assert (rc->details.pdf.off < MAX_RECORDS); + rc->details.pdf.jdata[rc->details.pdf.off] + = cover; + rc->details.pdf.docs[rc->details.pdf.off].form_name + = "_cover_"; + rc->details.pdf.docs[rc->details.pdf.off].form_version + = NULL; /* not yet supported! */ + rc->details.pdf.docs[rc->details.pdf.off].data + = rc->details.pdf.jdata[rc->details.pdf.off]; + rc->details.pdf.off++; +} + + +MHD_RESULT +TEH_handler_aml_attributes_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + struct ResponseContext *rctx = rc->rh_ctx; + int64_t limit = -20; + uint64_t offset; + struct TALER_NormalizedPaytoHashP h_payto; + uint64_t file_number; + + if (NULL == rctx) + { + rctx = GNUNET_new (struct ResponseContext); + rctx->rc = rc; + rc->rh_ctx = rctx; + rc->rh_cleaner = &free_rc; + } + else + { + if (NULL == rctx->response) + { + GNUNET_break (0); + return MHD_NO; + } + return MHD_queue_response (rc->connection, + rctx->http_status, + rctx->response); + } + if ( (NULL == args[0]) || + (NULL != args[1]) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &h_payto, + sizeof (h_payto))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PATH_SEGMENT_MALFORMED, + "h_payto"); + } + + TALER_MHD_parse_request_snumber (rc->connection, + "limit", + &limit); + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; + TALER_MHD_parse_request_number (rc->connection, + "offset", + &offset); + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->lookup_aml_file_number (TEH_plugin->cls, + &h_payto, + &file_number, + &rctx->is_wallet); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_aml_file_number"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Account unknown, return 404 */ + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_TARGET_ACCOUNT_UNKNOWN, + args[0]); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + { + const char *mime; + + mime = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "application/json"; + if (0 == strcmp (mime, + "application/json")) + { + rctx->format = RCF_JSON; + rctx->details.json = json_array (); + GNUNET_assert (NULL != rctx->details.json); + } + else if (0 == strcmp (mime, + "application/pdf")) + { + enum GNUNET_DB_QueryStatus qs; + + rctx->format = RCF_PDF; + rctx->details.pdf.global_attrs = json_object (); + GNUNET_assert (NULL != rctx->details.pdf.global_attrs); + if (NULL != TEH_global_pdf_form_data) + GNUNET_assert (0 == + json_object_update (rctx->details.pdf.global_attrs, + TEH_global_pdf_form_data)); + GNUNET_assert (0 == + json_object_set_new (rctx->details.pdf.global_attrs, + "FILE_NUMBER", + json_integer (file_number))); + /* Lookup latest AML decision & account rules & properties + to build the cover page */ + qs = TEH_plugin->lookup_aml_history (TEH_plugin->cls, + &h_payto, + UINT64_MAX, /* offset */ + -1, /* latest decision only */ + &build_cover_page, + rctx); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_aml_attributes"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* no decision was ever taken, build empty cover page */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No decisions taken, creating empty cover page\n"); + build_cover_page (rctx, + 0, + GNUNET_TIME_UNIT_ZERO_TS, + NULL, + NULL, + NULL, + NULL, + false, + false); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + else + { + GNUNET_break_op (0); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_NOT_ACCEPTABLE, + GNUNET_JSON_pack_string ("hint", + mime)); + } + } + + { + enum GNUNET_DB_QueryStatus qs; + + if (limit > MAX_RECORDS) + limit = MAX_RECORDS; + if (limit < -MAX_RECORDS) + limit = -MAX_RECORDS; + qs = TEH_plugin->select_aml_attributes ( + TEH_plugin->cls, + &h_payto, + offset, + limit, + &detail_cb, + rctx); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Obtained %d/%d attributes\n", + qs, + (int) (limit < 0 ? -limit : limit)); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_aml_attributes"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (RCF_JSON == rctx->format) + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + switch (rctx->format) + { + case RCF_JSON: + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_incref ("details", + rctx->details.json)); + case RCF_PDF: + /* Cover page: off of 0 is impossible */ + GNUNET_assert (0 != rctx->details.pdf.off); + for (unsigned int i = 0; i<rctx->details.pdf.off; i++) + { + /* The jdata[i] could be a *string* in case of attachments, + ignore those here! */ + if (NULL == rctx->details.pdf.docs[i].form_name) + { + GNUNET_assert (json_is_string (rctx->details.pdf.jdata[i])); + } + else + { + GNUNET_assert (json_is_object (rctx->details.pdf.jdata[i])); + GNUNET_assert (0 == + json_object_update_missing ( + rctx->details.pdf.jdata[i], + rctx->details.pdf.global_attrs) + ); + } + } + rctx->tc = TALER_MHD_typst (TALER_EXCHANGE_project_data (), + TEH_cfg, + false, + "exchange", + rctx->details.pdf.off, + rctx->details.pdf.docs, + &pdf_cb, + rctx); + if (NULL == rctx->tc) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client requested PDF, but Typst is unavailable\n"); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK, + NULL); + } + GNUNET_CONTAINER_DLL_insert (rctx_head, + rctx_tail, + rctx); + MHD_suspend_connection (rc->connection); + return MHD_YES; + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-exchange-httpd_aml-attributes_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.h b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.h @@ -0,0 +1,52 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-attributes-H_NORMALIZED_PAYTO.h + * @brief Handle /aml/$OFFICER_PUB/attributes/$H_PAYTO requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_ATTRIBUTES_H_NORMALIZED_PAYTO_H +#define TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_ATTRIBUTES_H_NORMALIZED_PAYTO_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/aml/$OFFICER_PUB/attributes/$H_PAYTO" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching decisions. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be the state) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_attributes_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +/** + * Stop async running attribute GET requests. + */ +void +TEH_handler_aml_attributes_get_cleanup (void); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c @@ -0,0 +1,205 @@ +/* + This file is part of TALER + Copyright (C) 2024, 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c + * @brief Return summary information about AML decisions + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h" +#include "taler-exchange-httpd_get-metrics.h" + +/** + * Maximum number of records we return in one request. + */ +#define MAX_RECORDS 1024 + +/** + * Return AML status. + * + * @param cls closure + * @param row_id current row in AML status table + * @param justification human-readable reason for the decision + * @param h_payto account for which the attribute data is stored + * @param decision_time when was the decision taken + * @param expiration_time when will the rules expire + * @param jproperties properties set for the account, + * NULL if no properties were set + * @param to_investigate true if AML staff should look at the account + * @param is_active true if this is the currently active decision about the account + * @param is_wallet true if the @a h_payto is for a wallet + * @param payto the payto URI of the account affected by the decision + * @param account_rules current active rules for the account + */ +static void +record_cb ( + void *cls, + uint64_t row_id, + const char *justification, + const struct TALER_NormalizedPaytoHashP *h_payto, + struct GNUNET_TIME_Timestamp decision_time, + struct GNUNET_TIME_Absolute expiration_time, + const json_t *jproperties, + bool to_investigate, + bool is_active, + bool is_wallet, + struct TALER_FullPayto payto, + const json_t *account_rules) +{ + json_t *records = cls; + + GNUNET_assert ( + 0 == + json_array_append_new ( + records, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + TALER_JSON_pack_full_payto ("full_payto", + payto), + GNUNET_JSON_pack_bool ("is_wallet", + is_wallet), + GNUNET_JSON_pack_int64 ("rowid", + row_id), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("justification", + justification)), + GNUNET_JSON_pack_timestamp ("decision_time", + decision_time), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("properties", + (json_t *) jproperties)), + GNUNET_JSON_pack_object_incref ("limits", + (json_t *) account_rules), + GNUNET_JSON_pack_bool ("to_investigate", + to_investigate), + GNUNET_JSON_pack_bool ("is_active", + is_active) + ))); +} + + +MHD_RESULT +TEH_handler_aml_decisions_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + int64_t limit = -20; + uint64_t offset; + struct TALER_NormalizedPaytoHashP h_payto; + bool have_payto = false; + enum TALER_EXCHANGE_YesNoAll active_filter; + enum TALER_EXCHANGE_YesNoAll investigation_filter; + + if (NULL != args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[0]); + } + TALER_MHD_parse_request_snumber (rc->connection, + "limit", + &limit); + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; + TALER_MHD_parse_request_number (rc->connection, + "offset", + &offset); + if (offset > INT64_MAX) + { + GNUNET_break_op (0); /* broken client */ + offset = INT64_MAX; + } + TALER_MHD_parse_request_arg_auto (rc->connection, + "h_payto", + &h_payto, + have_payto); + TALER_MHD_parse_request_yna (rc->connection, + "active", + TALER_EXCHANGE_YNA_ALL, + &active_filter); + TALER_MHD_parse_request_yna (rc->connection, + "investigation", + TALER_EXCHANGE_YNA_ALL, + &investigation_filter); + { + json_t *records; + enum GNUNET_DB_QueryStatus qs; + + records = json_array (); + GNUNET_assert (NULL != records); + if (limit > MAX_RECORDS) + limit = MAX_RECORDS; + if (limit < -MAX_RECORDS) + limit = -MAX_RECORDS; + qs = TEH_plugin->select_aml_decisions ( + TEH_plugin->cls, + have_payto + ? &h_payto + : NULL, + investigation_filter, + active_filter, + offset, + limit, + &record_cb, + records); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + json_decref (records); + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_aml_decisions"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + json_decref (records); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("records", + records)); + } +} + + +/* end of taler-exchange-httpd_aml-decisions_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.c @@ -0,0 +1,173 @@ +/* + This file is part of TALER + Copyright (C) 2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.c + * @brief Return summary information about KYC statistics + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.h" +#include "taler-exchange-httpd_get-metrics.h" + +/** + * Maximum number of statistics that can be requested in one go. + */ +#define MAX_STATS 64 + + +/** + * Function called with AML statistics (counters). + * + * @param cls JSON array to build + * @param name name of the counter + * @param cnt number of events for @a name in the query range + */ +static void +add_stat ( + void *cls, + const char *name, + uint64_t cnt) +{ + json_t *stats = cls; + + GNUNET_assert ( + 0 == + json_array_append_new ( + stats, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("event", + name), + GNUNET_JSON_pack_uint64 ("counter", + cnt)))); +} + + +MHD_RESULT +TEH_handler_aml_kyc_statistics_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + struct GNUNET_TIME_Timestamp start_date = GNUNET_TIME_UNIT_ZERO_TS; + struct GNUNET_TIME_Timestamp end_date = GNUNET_TIME_UNIT_FOREVER_TS; + const char *all_names = args[0]; + json_t *stats; + size_t max_names; + + if ( (NULL == args[0]) || + (NULL != args[1]) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + TALER_MHD_parse_request_timestamp (rc->connection, + "start_date", + &start_date); + TALER_MHD_parse_request_timestamp (rc->connection, + "end_date", + &end_date); + if (GNUNET_TIME_absolute_is_never (end_date.abs_time)) + end_date = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_SECONDS)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Looking for stats on %s from [%llu,%llu)\n", + all_names, + (unsigned long long) start_date.abs_time.abs_value_us, + (unsigned long long) end_date.abs_time.abs_value_us); + /* Estimate number of names in 'all_names' */ + max_names = 1; + for (size_t i = 0; '\0' != all_names[i]; i++) + if (' ' == all_names[i]) + max_names++; + if (max_names > MAX_STATS) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_URI_TOO_LONG, + TALER_EC_GENERIC_URI_TOO_LONG, + rc->url); + } + stats = json_array (); + GNUNET_assert (NULL != stats); + { + char *buf; + const char *names[GNUNET_NZL (max_names)]; + enum GNUNET_DB_QueryStatus qs; + size_t num_names = 0; + + buf = GNUNET_strdup (all_names); + for (const char *tok = strtok (buf, + " "); + NULL != tok; + tok = strtok (NULL, + " ")) + { + GNUNET_assert (num_names < max_names); + names[num_names++] = tok; + } + qs = TEH_plugin->select_aml_statistics ( + TEH_plugin->cls, + num_names, + names, + start_date, + end_date, + &add_stat, + stats); + GNUNET_free (buf); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_aml_statistics"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("statistics", + stats)); + } +} + + +/* end of taler-exchange-httpd_aml-statistics_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.h b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.h @@ -0,0 +1,45 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-kyc-statistics-NAMES.h + * @brief Handle /aml/$OFFICER_PUB/kyc-statistics/$NAME requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_KYC_STATISTICS_NAMES_H +#define TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_KYC_STATISTICS_NAMES_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/aml/$OFFICER_PUB/kyc-statistics/$NAME" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching decisions. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (name of the statistic) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_kyc_statistics_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.c @@ -0,0 +1,180 @@ +/* + This file is part of TALER + Copyright (C) 2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.c + * @brief Return information about legitimization measures + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.h" +#include "taler-exchange-httpd_get-metrics.h" + +/** + * Maximum number of measures we return in one request. + */ +#define MAX_MEASURES 1024 + +/** + * Return LEGITIMIZATION measure. + * + * @param cls closure + * @param h_payto hash of account the measure applies to + * @param start_time when was the process started + * @param jmeasures object of type ``LegitimizationMeasures`` + * @param is_finished true if the measure was finished + * @param measure_serial_id row ID of the measure in the exchange table + */ +static void +record_cb ( + void *cls, + struct TALER_NormalizedPaytoHashP *h_payto, + struct GNUNET_TIME_Absolute start_time, + const json_t *jmeasures, + bool is_finished, + uint64_t measure_serial_id) +{ + json_t *measures = cls; + + GNUNET_assert ( + 0 == + json_array_append_new ( + measures, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + GNUNET_JSON_pack_uint64 ("rowid", + measure_serial_id), + GNUNET_JSON_pack_timestamp ("start_time", + GNUNET_TIME_absolute_to_timestamp ( + start_time)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("measures", + (json_t *) jmeasures)), + GNUNET_JSON_pack_bool ("is_finished", + is_finished) + ))); +} + + +MHD_RESULT +TEH_handler_aml_legitimization_measures_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + int64_t limit = -20; + uint64_t offset; + struct TALER_NormalizedPaytoHashP h_payto; + bool have_payto = false; + enum TALER_EXCHANGE_YesNoAll active_filter; + + if (NULL != args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[0]); + } + TALER_MHD_parse_request_snumber (rc->connection, + "limit", + &limit); + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; + TALER_MHD_parse_request_number (rc->connection, + "offset", + &offset); + if (offset > INT64_MAX) + { + GNUNET_break_op (0); /* broken client */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received invalid offset %llu > %llu\n", + (unsigned long long) offset, + (unsigned long long) INT64_MAX); + offset = INT64_MAX; + } + TALER_MHD_parse_request_arg_auto (rc->connection, + "h_payto", + &h_payto, + have_payto); + TALER_MHD_parse_request_yna (rc->connection, + "active", + TALER_EXCHANGE_YNA_ALL, + &active_filter); + { + json_t *measures; + enum GNUNET_DB_QueryStatus qs; + + measures = json_array (); + GNUNET_assert (NULL != measures); + if (limit > MAX_MEASURES) + limit = MAX_MEASURES; + if (limit < -MAX_MEASURES) + limit = -MAX_MEASURES; + qs = TEH_plugin->select_aml_measures ( + TEH_plugin->cls, + have_payto + ? &h_payto + : NULL, + active_filter, + offset, + limit, + &record_cb, + measures); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + json_decref (measures); + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_aml_measures"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + json_decref (measures); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("measures", + measures)); + } +} + + +/* end of taler-exchange-httpd_legitimization-measures_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.h b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.h @@ -0,0 +1,43 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-legitimizations.h + * @brief Handle /aml/$OFFICER_PUB/legitimizations requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_LEGITIMIZATIONS_H +#define TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_LEGITIMIZATIONS_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + +/** + * Handle a GET "/aml/$OFFICER_PUB/legitimizations" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching measuress. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be the state) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_legitimization_measures_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-measures.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-measures.c @@ -0,0 +1,75 @@ +/* + This file is part of TALER + Copyright (C) 2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-measures.c + * @brief Return summary information about KYC measures + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-measures.h" + + +MHD_RESULT +TEH_handler_aml_measures_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + static json_t *roots; + static json_t *programs; + static json_t *checks; + static json_t *default_rules; + + if (NULL == roots) + { + TALER_KYCLOGIC_get_measure_configuration (&roots, + &programs, + &checks, + &default_rules); + } + if (NULL != args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_incref ("default_rules", + default_rules), + GNUNET_JSON_pack_object_incref ("roots", + roots), + GNUNET_JSON_pack_object_incref ("programs", + programs), + GNUNET_JSON_pack_object_incref ("checks", + checks)); +} + + +/* end of taler-exchange-httpd_aml-measures_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-measures.h b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-measures.h @@ -0,0 +1,45 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-measures.h + * @brief Handle /aml/$OFFICER_PUB/measures requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_MEASURES_H +#define TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_MEASURES_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/aml/$OFFICER_PUB/measures" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching decisions. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be empty) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_measures_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.c @@ -0,0 +1,258 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.c + * @brief Return information about exchange wire transfers + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.h" +#include "taler-exchange-httpd_get-metrics.h" + + +enum TransferType +{ + TT_CREDIT, + TT_DEBIT, + TT_KYCAUTH +}; + + +/** + * Maximum number of transfers we return in one request. + */ +#define MAX_TRANSFERS 1024 + +/** + * Return transfer data. + * + * @param cls closure + * @param row_id current row in AML status table + * @param payto_uri account involved with the wire transfer + * @param execution_time when was the transfer made + * @param amount wire amount of the transfer + */ +static void +record_cb ( + void *cls, + uint64_t row_id, + const char *payto_uri, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *amount) +{ + json_t *transfers = cls; + + GNUNET_assert ( + 0 == + json_array_append_new ( + transfers, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("payto_uri", + payto_uri), + GNUNET_JSON_pack_int64 ("rowid", + row_id), + GNUNET_JSON_pack_timestamp ("execution_time", + GNUNET_TIME_absolute_to_timestamp ( + execution_time)), + TALER_JSON_pack_amount ("amount", + amount) + ))); +} + + +/** + * Handle HTTP request by AML officer for transfer data. + * + * @param rc request context + * @param officer_pub the AML officer + * @param tt which type of transfer data to return + * @param args further arguments provided (should be empty) + * @return MHD status + */ +static MHD_RESULT +aml_transfer_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + enum TransferType tt, + const char *const args[]) +{ + int64_t limit = -20; + uint64_t offset; + struct TALER_Amount threshold; + struct TALER_NormalizedPaytoHashP h_payto; + bool have_payto = false; + + if (NULL != args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[0]); + } + TALER_MHD_parse_request_arg_auto (rc->connection, + "h_payto", + &h_payto, + have_payto); + TALER_MHD_parse_request_snumber (rc->connection, + "limit", + &limit); + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; + TALER_MHD_parse_request_number (rc->connection, + "offset", + &offset); + if (offset > INT64_MAX) + { + GNUNET_break_op (0); /* broken client */ + offset = INT64_MAX; + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &threshold)); + TALER_MHD_parse_request_amount (rc->connection, + "threshold", + &threshold); + { + json_t *transfers; + enum GNUNET_DB_QueryStatus qs; + const char *query; + + transfers = json_array (); + GNUNET_assert (NULL != transfers); + if (limit > MAX_TRANSFERS) + limit = MAX_TRANSFERS; + if (limit < -MAX_TRANSFERS) + limit = -MAX_TRANSFERS; + switch (tt) + { + case TT_DEBIT: + qs = TEH_plugin->select_exchange_debit_transfers ( + TEH_plugin->cls, + &threshold, + offset, + limit, + have_payto ? &h_payto : NULL, + &record_cb, + transfers); + query = "select_exchange_debit_transfers"; + break; + case TT_CREDIT: + qs = TEH_plugin->select_exchange_credit_transfers ( + TEH_plugin->cls, + &threshold, + offset, + limit, + have_payto ? &h_payto : NULL, + &record_cb, + transfers); + query = "select_exchange_credit_transfers"; + break; + case TT_KYCAUTH: + qs = TEH_plugin->select_exchange_kycauth_transfers ( + TEH_plugin->cls, + &threshold, + offset, + limit, + have_payto ? &h_payto : NULL, + &record_cb, + transfers); + query = "select_exchange_kycauth_transfers"; + break; + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + json_decref (transfers); + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + query); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + json_decref (transfers); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("transfers", + transfers)); + } +} + + +MHD_RESULT +TEH_handler_aml_transfer_credit_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + return aml_transfer_get (rc, + officer_pub, + TT_CREDIT, + args); +} + + +MHD_RESULT +TEH_handler_aml_transfer_kycauth_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + return aml_transfer_get (rc, + officer_pub, + TT_KYCAUTH, + args); +} + + +MHD_RESULT +TEH_handler_aml_transfer_debit_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + return aml_transfer_get (rc, + officer_pub, + TT_DEBIT, + args); +} + + +/* end of taler-exchange-httpd_aml-decisions_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.h b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.h @@ -0,0 +1,79 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-aml-OFFICER_PUB-transfers.h + * @brief Handle /aml/$OFFICER_PUB/transfer-* requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_TRANSFERS_H +#define TALER_EXCHANGE_HTTPD_GET_AML_OFFICER_PUB_TRANSFERS_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/aml/$OFFICER_PUB/transfer-credit" request. Parses the + * request details, checks the signatures and if appropriately authorized + * returns the matching wire transfers. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be empty) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_transfer_credit_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +/** + * Handle a GET "/aml/$OFFICER_PUB/transfer-kycauth" request. Parses the + * request details, checks the signatures and if appropriately authorized + * returns the matching wire transfers. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be empty) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_transfer_kycauth_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +/** + * Handle a GET "/aml/$OFFICER_PUB/transfer-debit" request. Parses the + * request details, checks the signatures and if appropriately authorized + * returns the matching wire transfers. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be empty) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_transfer_debit_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c b/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c @@ -0,0 +1,763 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-coins-COIN_PUB-history.c + * @brief Handle GET /coins/$COIN_PUB/history requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_mhd_lib.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-coins-COIN_PUB-history.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Add the headers we want to set for every response. + * + * @param cls the key state to use + * @param[in,out] response the response to modify + */ +static void +add_response_headers (void *cls, + struct MHD_Response *response) +{ + (void) cls; + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-cache")); +} + + +/** + * Compile the transaction history of a coin into a JSON object. + * + * @param coin_pub public key of the coin + * @param tl transaction history to JSON-ify + * @return json representation of the @a rh, NULL on error + */ +static json_t * +compile_transaction_history ( + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_EXCHANGEDB_TransactionList *tl) +{ + json_t *history; + + history = json_array (); + if (NULL == history) + { + GNUNET_break (0); /* out of memory!? */ + return NULL; + } + for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; + NULL != pos; + pos = pos->next) + { + switch (pos->type) + { + case TALER_EXCHANGEDB_TT_DEPOSIT: + { + const struct TALER_EXCHANGEDB_DepositListEntry *deposit = + pos->details.deposit; + struct TALER_MerchantWireHashP h_wire; + + TALER_merchant_wire_signature_hash (deposit->receiver_wire_account, + &deposit->wire_salt, + &h_wire); +#if ENABLE_SANITY_CHECKS + /* internal sanity check before we hand out a bogus sig... */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_deposit_verify ( + &deposit->amount_with_fee, + &deposit->deposit_fee, + &h_wire, + &deposit->h_contract_terms, + deposit->no_wallet_data_hash + ? NULL + : &deposit->wallet_data_hash, + deposit->no_age_commitment + ? NULL + : &deposit->h_age_commitment, + &deposit->h_policy, + &deposit->h_denom_pub, + deposit->timestamp, + &deposit->merchant_pub, + deposit->refund_deadline, + coin_pub, + &deposit->csig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } +#endif + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "DEPOSIT"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &deposit->amount_with_fee), + TALER_JSON_pack_amount ("deposit_fee", + &deposit->deposit_fee), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &deposit->merchant_pub), + GNUNET_JSON_pack_timestamp ("timestamp", + deposit->timestamp), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("refund_deadline", + deposit->refund_deadline)), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &deposit->h_contract_terms), + GNUNET_JSON_pack_data_auto ("h_wire", + &h_wire), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &deposit->h_denom_pub), + GNUNET_JSON_pack_allow_null ( + deposit->has_policy + ? GNUNET_JSON_pack_data_auto ("h_policy", + &deposit->h_policy) + : GNUNET_JSON_pack_string ( + "h_policy", + NULL)), + GNUNET_JSON_pack_allow_null ( + deposit->no_wallet_data_hash + ? GNUNET_JSON_pack_string ( + "wallet_data_hash", + NULL) + : GNUNET_JSON_pack_data_auto ("wallet_data_hash", + &deposit->wallet_data_hash)), + GNUNET_JSON_pack_allow_null ( + deposit->no_age_commitment + ? GNUNET_JSON_pack_string ( + "h_age_commitment", + NULL) + : GNUNET_JSON_pack_data_auto ("h_age_commitment", + &deposit->h_age_commitment)), + GNUNET_JSON_pack_data_auto ("coin_sig", + &deposit->csig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + case TALER_EXCHANGEDB_TT_MELT: + { + const struct TALER_EXCHANGEDB_MeltListEntry *melt = + pos->details.melt; + const struct TALER_AgeCommitmentHashP *phac; + const struct TALER_BlindingMasterSeedP *pbs; + +#if ENABLE_SANITY_CHECKS + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_melt_verify ( + &melt->amount_with_fee, + &melt->melt_fee, + &melt->rc, + &melt->h_denom_pub, + &melt->h_age_commitment, + coin_pub, + &melt->coin_sig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } +#endif + phac = (melt->no_age_commitment) + ? NULL + : &melt->h_age_commitment; + pbs = melt->no_blinding_seed + ? NULL + : &melt->blinding_seed; + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "MELT"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &melt->amount_with_fee), + TALER_JSON_pack_amount ("melt_fee", + &melt->melt_fee), + GNUNET_JSON_pack_data_auto ("rc", + &melt->rc), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &melt->h_denom_pub), + GNUNET_JSON_pack_data_auto ("refresh_seed", + &melt->refresh_seed), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("blinding_seed", + pbs)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + phac)), + GNUNET_JSON_pack_data_auto ("coin_sig", + &melt->coin_sig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_TT_REFUND: + { + const struct TALER_EXCHANGEDB_RefundListEntry *refund = + pos->details.refund; + struct TALER_Amount value; + +#if ENABLE_SANITY_CHECKS + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_merchant_refund_verify ( + coin_pub, + &refund->h_contract_terms, + refund->rtransaction_id, + &refund->refund_amount, + &refund->merchant_pub, + &refund->merchant_sig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } +#endif + if (0 > + TALER_amount_subtract (&value, + &refund->refund_amount, + &refund->refund_fee)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "REFUND"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &value), + TALER_JSON_pack_amount ("refund_fee", + &refund->refund_fee), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &refund->h_contract_terms), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &refund->merchant_pub), + GNUNET_JSON_pack_uint64 ("rtransaction_id", + refund->rtransaction_id), + GNUNET_JSON_pack_data_auto ("merchant_sig", + &refund->merchant_sig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER: + { + struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = + pos->details.old_coin_recoup; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_refresh_sign ( + &TEH_keys_exchange_sign_, + pr->timestamp, + &pr->value, + &pr->coin.coin_pub, + &pr->old_coin_pub, + &epub, + &esig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + /* NOTE: we could also provide coin_pub's coin_sig, denomination key hash and + the denomination key's RSA signature over coin_pub, but as the + wallet should really already have this information (and cannot + check or do anything with it anyway if it doesn't), it seems + strictly unnecessary. */ + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RECOUP-REFRESH-RECEIVER"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &pr->value), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("coin_pub", + &pr->coin.coin_pub), + GNUNET_JSON_pack_timestamp ("timestamp", + pr->timestamp)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + case TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW: + { + const struct TALER_EXCHANGEDB_RecoupListEntry *recoup = + pos->details.recoup; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_sign ( + &TEH_keys_exchange_sign_, + recoup->timestamp, + &recoup->value, + coin_pub, + &recoup->reserve_pub, + &epub, + &esig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RECOUP-WITHDRAW"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &recoup->value), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &recoup->reserve_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &recoup->coin_sig), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &recoup->h_denom_pub), + GNUNET_JSON_pack_data_auto ("coin_blind", + &recoup->coin_blind), + // FIXME-9828: spec says we should have h_commitment? + // FIXME-9828: spec says we should have coin_index? + GNUNET_JSON_pack_data_auto ("reserve_pub", + &recoup->reserve_pub), + GNUNET_JSON_pack_timestamp ("timestamp", + recoup->timestamp)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: + { + struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = + pos->details.recoup_refresh; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_refresh_sign ( + &TEH_keys_exchange_sign_, + pr->timestamp, + &pr->value, + coin_pub, + &pr->old_coin_pub, + &epub, + &esig)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + /* NOTE: we could also provide coin_pub's coin_sig, denomination key + hash and the denomination key's RSA signature over coin_pub, but as + the wallet should really already have this information (and cannot + check or do anything with it anyway if it doesn't), it seems + strictly unnecessary. */ + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RECOUP-REFRESH"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &pr->value), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("old_coin_pub", + &pr->old_coin_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &pr->coin_sig), + // FIXME-#9828: spec says to return h_denom_pub + GNUNET_JSON_pack_data_auto ("coin_blind", + &pr->coin_blind), + // FIXME-#9828: spec says to return h_commitment + // FIXME-#9828: spec says to return coin_index + // FIXME-#9828: spec says to return new_coin_blinding_secret + // FIXME-#9828: spec says to return new_coin_ev + GNUNET_JSON_pack_timestamp ("timestamp", + pr->timestamp)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + + case TALER_EXCHANGEDB_TT_PURSE_DEPOSIT: + { + struct TALER_EXCHANGEDB_PurseDepositListEntry *pd + = pos->details.purse_deposit; + const struct TALER_AgeCommitmentHashP *phac = NULL; + + if (! pd->no_age_commitment) + phac = &pd->h_age_commitment; + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "PURSE-DEPOSIT"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &pd->amount), + GNUNET_JSON_pack_string ("exchange_base_url", + NULL == pd->exchange_base_url + ? TEH_base_url + : pd->exchange_base_url), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("h_age_commitment", + phac)), + TALER_JSON_pack_amount ("deposit_fee", + &pd->deposit_fee), + GNUNET_JSON_pack_data_auto ("purse_pub", + &pd->purse_pub), + GNUNET_JSON_pack_bool ("refunded", + pd->refunded), + GNUNET_JSON_pack_data_auto ("coin_sig", + &pd->coin_sig), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &pd->h_denom_pub)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + + case TALER_EXCHANGEDB_TT_PURSE_REFUND: + { + const struct TALER_EXCHANGEDB_PurseRefundListEntry *prefund = + pos->details.purse_refund; + struct TALER_Amount value; + enum TALER_ErrorCode ec; + struct TALER_ExchangePublicKeyP epub; + struct TALER_ExchangeSignatureP esig; + + if (0 > + TALER_amount_subtract (&value, + &prefund->refund_amount, + &prefund->refund_fee)) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + ec = TALER_exchange_online_purse_refund_sign ( + &TEH_keys_exchange_sign_, + &value, + &prefund->refund_fee, + coin_pub, + &prefund->purse_pub, + &epub, + &esig); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "PURSE-REFUND"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("amount", + &value), + TALER_JSON_pack_amount ("refund_fee", + &prefund->refund_fee), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &esig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &epub), + GNUNET_JSON_pack_data_auto ("purse_pub", + &prefund->purse_pub)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + } + break; + + case TALER_EXCHANGEDB_TT_RESERVE_OPEN: + { + struct TALER_EXCHANGEDB_ReserveOpenListEntry *role + = pos->details.reserve_open; + + if (0 != + json_array_append_new ( + history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RESERVE-OPEN-DEPOSIT"), + GNUNET_JSON_pack_uint64 ("history_offset", + pos->coin_history_id), + TALER_JSON_pack_amount ("coin_contribution", + &role->coin_contribution), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &role->reserve_sig), + GNUNET_JSON_pack_data_auto ("coin_sig", + &role->coin_sig)))) + { + GNUNET_break (0); + json_decref (history); + return NULL; + } + break; + } + } + } + return history; +} + + +MHD_RESULT +TEH_handler_coins_get (struct TEH_RequestContext *rc, + const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + struct TALER_EXCHANGEDB_TransactionList *tl = NULL; + uint64_t start_off = 0; + uint64_t etag_in; + uint64_t etag_out; + char etagp[24]; + struct MHD_Response *resp; + unsigned int http_status; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_Amount balance; + + TALER_MHD_parse_request_number (rc->connection, + "start", + &start_off); + /* Check signature */ + { + struct TALER_CoinSpendSignatureP coin_sig; + bool required = true; + + TALER_MHD_parse_request_header_auto (rc->connection, + TALER_COIN_HISTORY_SIGNATURE_HEADER, + &coin_sig, + required); + if (GNUNET_OK != + TALER_wallet_coin_history_verify (start_off, + coin_pub, + &coin_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_COIN_HISTORY_BAD_SIGNATURE, + NULL); + } + } + + /* Get etag */ + { + const char *etags; + + etags = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != etags) + { + char dummy; + unsigned long long ev; + + if (1 != sscanf (etags, + "\"%llu\"%c", + &ev, + &dummy)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client send malformed `If-None-Match' header `%s'\n", + etags); + etag_in = start_off; + } + else + { + etag_in = (uint64_t) ev; + } + } + else + { + etag_in = start_off; + } + } + + /* Get history from DB between etag and now */ + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, + true, + coin_pub, + start_off, + etag_in, + &etag_out, + &balance, + &h_denom_pub, + &tl); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_coin_history"); + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_coin_history"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Handled below */ + break; + } + } + + GNUNET_snprintf (etagp, + sizeof (etagp), + "\"%llu\"", + (unsigned long long) etag_out); + if (etag_in == etag_out) + { + return TEH_RESPONSE_reply_not_modified (rc->connection, + etagp, + &add_response_headers, + NULL); + } + if (NULL == tl) + { + /* 204: empty history */ + resp = MHD_create_response_from_buffer_static (0, + ""); + http_status = MHD_HTTP_NO_CONTENT; + } + else + { + /* 200: regular history */ + json_t *history; + + history = compile_transaction_history (coin_pub, + tl); + TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, + tl); + tl = NULL; + if (NULL == history) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + "Failed to compile coin history"); + } + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &h_denom_pub), + TALER_JSON_pack_amount ("balance", + &balance), + GNUNET_JSON_pack_array_steal ("history", + history)); + http_status = MHD_HTTP_OK; + } + add_response_headers (NULL, + resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etagp)); + { + MHD_RESULT ret; + + ret = MHD_queue_response (rc->connection, + http_status, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } +} + + +/* end of taler-exchange-httpd_coins_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.h b/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.h @@ -0,0 +1,53 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-coins-COIN_PUB-history.h + * @brief Handle GET /coins/$COIN_PUB requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_COINS_COIN_PUB_HISTORY_H +#define TALER_EXCHANGE_HTTPD_GET_COINS_COIN_PUB_HISTORY_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown reserves-get subsystem. Resumes all + * suspended long-polling clients and cleans up + * data structures. + */ +void +TEH_reserves_get_cleanup (void); + + +/** + * Handle a GET "/coins/$COIN_PUB/history" request. Parses the + * given "coins_pub" in @a args (which should contain the + * EdDSA public key of a reserve) and then respond with the + * transaction history of the coin. + * + * @param rc request context + * @param coin_pub public key of the coin + * @return MHD result code + */ +MHD_RESULT +TEH_handler_coins_get (struct TEH_RequestContext *rc, + const struct TALER_CoinSpendPublicKeyP *coin_pub); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-config.c b/src/exchange/taler-exchange-httpd_get-config.c @@ -0,0 +1,77 @@ +/* + This file is part of TALER + Copyright (C) 2015-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-config.c + * @brief Handle /config requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_json_lib.h> +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-config.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include <jansson.h> + + +MHD_RESULT +TEH_handler_config (struct TEH_RequestContext *rc, + const char *const args[]) +{ + static struct MHD_Response *resp; + + (void) args; + if (NULL == resp) + { + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ( + "wallet_balance_limit_without_kyc", + TALER_KYCLOGIC_get_wallet_thresholds ())), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("shopping_url", + TEH_shopping_url)), + GNUNET_JSON_pack_object_steal ( + "currency_specification", + TALER_JSON_currency_specs_to_json (TEH_cspec)), + GNUNET_JSON_pack_string ( + "currency", + TEH_currency), + GNUNET_JSON_pack_string ( + "name", + "taler-exchange"), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "aml_spa_dialect", + TEH_aml_spa_dialect)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("shopping_url", + TEH_shopping_url)), + GNUNET_JSON_pack_string ( + "implementation", + "urn:net:taler:specs:taler-exchange:c-reference"), + GNUNET_JSON_pack_string ( + "version", + EXCHANGE_PROTOCOL_VERSION)); + } + return MHD_queue_response (rc->connection, + MHD_HTTP_OK, + resp); +} + + +/* end of taler-exchange-httpd_config.c */ diff --git a/src/exchange/taler-exchange-httpd_get-config.h b/src/exchange/taler-exchange-httpd_get-config.h @@ -0,0 +1,58 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of EXCHANGEABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-config.h + * @brief headers for /config handler + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_CONFIG_H +#define TALER_EXCHANGE_HTTPD_GET_CONFIG_H +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Taler protocol version in the format CURRENT:REVISION:AGE + * as used by GNU libtool. See + * https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html + * + * Please be very careful when updating and follow + * https://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html#Updating-version-info + * precisely. Note that this version has NOTHING to do with the + * release version, and the format is NOT the same that semantic + * versioning uses either. + * + * When changing this version, you likely want to also update + * #TALER_PROTOCOL_CURRENT and #TALER_PROTOCOL_AGE in + * exchange_api_handle.c! + * + * Returned via both /config and /keys endpoints. + */ +#define EXCHANGE_PROTOCOL_VERSION "34:0:0" + + +/** + * Manages a /config call. + * + * @param rc context of the handler + * @param[in,out] args remaining arguments (ignored) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_config (struct TEH_RequestContext *rc, + const char *const args[]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-contracts-CONTRACT_PUB.c b/src/exchange/taler-exchange-httpd_get-contracts-CONTRACT_PUB.c @@ -0,0 +1,99 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-contracts-CONTRACT_PUB.c + * @brief Handle GET /contracts/$C_PUB requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_get-contracts-CONTRACT_PUB.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_responses.h" + + +MHD_RESULT +TEH_handler_contracts_get (struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct TALER_ContractDiffiePublicP contract_pub; + struct TALER_PurseContractPublicKeyP purse_pub; + void *econtract; + size_t econtract_size; + enum GNUNET_DB_QueryStatus qs; + struct TALER_PurseContractSignatureP econtract_sig; + MHD_RESULT res; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &contract_pub, + sizeof (contract_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB, + args[0]); + } + + qs = TEH_plugin->select_contract (TEH_plugin->cls, + &contract_pub, + &purse_pub, + &econtract_sig, + &econtract_size, + &econtract); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_contract"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_contract"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_CONTRACTS_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* handled below */ + } + res = TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("purse_pub", + &purse_pub), + GNUNET_JSON_pack_data_auto ("econtract_sig", + &econtract_sig), + GNUNET_JSON_pack_data_varsize ("econtract", + econtract, + econtract_size)); + GNUNET_free (econtract); + return res; +} + + +/* end of taler-exchange-httpd_contract.c */ diff --git a/src/exchange/taler-exchange-httpd_get-contracts-CONTRACT_PUB.h b/src/exchange/taler-exchange-httpd_get-contracts-CONTRACT_PUB.h @@ -0,0 +1,44 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-contracts-CONTRACT_PUB.h + * @brief Handle /coins/$COIN_PUB/contract requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_CONTRACTS_CONTRACT_PUB_H +#define TALER_EXCHANGE_HTTPD_GET_CONTRACTS_CONTRACT_PUB_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/contracts/$C_PUB" request. Returns the + * encrypted contract. + * + * @param rc request context + * @param args array of additional options (length: 1, first is the contract_pub) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_contracts_get (struct TEH_RequestContext *rc, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-deposits.c b/src/exchange/taler-exchange-httpd_get-deposits.c @@ -0,0 +1,536 @@ +/* + This file is part of TALER + Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-deposits.c + * @brief Handle wire deposit tracking-related requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_dbevents.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_util.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-deposits.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for #handle_wtid_data. + */ +struct DepositWtidContext +{ + + /** + * Kept in a DLL. + */ + struct DepositWtidContext *next; + + /** + * Kept in a DLL. + */ + struct DepositWtidContext *prev; + + /** + * Context for the request we are processing. + */ + struct TEH_RequestContext *rc; + + /** + * Subscription for the database event we are waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Hash over the proposal data of the contract for which this deposit is made. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Hash over the wiring information of the merchant. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * The Merchant's public key. The deposit inquiry request is to be + * signed by the corresponding private key (using EdDSA). + */ + struct TALER_MerchantPublicKeyP merchant; + + /** + * Public key for KYC operations on the target bank + * account for the wire transfer. All zero if no + * public key is accepted yet. In that case, the + * client should use the @e merchant public key for + * the KYC auth wire transfer. + */ + union TALER_AccountPublicKeyP account_pub; + + /** + * The coin's public key. This is the value that must have been + * signed (blindly) by the Exchange. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Set by #handle_wtid data to the wire transfer ID. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Signature by the merchant. + */ + struct TALER_MerchantSignatureP merchant_sig; + + /** + * Set by #handle_wtid data to the coin's contribution to the wire transfer. + */ + struct TALER_Amount coin_contribution; + + /** + * Set by #handle_wtid data to the fee charged to the coin. + */ + struct TALER_Amount coin_fee; + + /** + * Set by #handle_wtid data to the wire transfer execution time. + */ + struct GNUNET_TIME_Timestamp execution_time; + + /** + * Timeout of the request, for long-polling. + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Set by #handle_wtid to the coin contribution to the transaction + * (that is, @e coin_contribution minus @e coin_fee). + */ + struct TALER_Amount coin_delta; + + /** + * KYC status information for the receiving account. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * #GNUNET_YES if we were suspended, #GNUNET_SYSERR + * if we were woken up due to shutdown. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * What do we long-poll for? Defaults to + * #TALER_DGLPT_OK if not given. + */ + enum TALER_DepositGetLongPollTarget lpt; +}; + + +/** + * Head of DLL of suspended requests. + */ +static struct DepositWtidContext *dwc_head; + +/** + * Tail of DLL of suspended requests. + */ +static struct DepositWtidContext *dwc_tail; + + +void +TEH_deposits_get_cleanup () +{ + struct DepositWtidContext *n; + + for (struct DepositWtidContext *ctx = dwc_head; + NULL != ctx; + ctx = n) + { + n = ctx->next; + GNUNET_assert (GNUNET_YES == ctx->suspended); + ctx->suspended = GNUNET_SYSERR; + MHD_resume_connection (ctx->rc->connection); + GNUNET_CONTAINER_DLL_remove (dwc_head, + dwc_tail, + ctx); + } +} + + +/** + * A merchant asked for details about a deposit. Provide + * them. Generates the 200 reply. + * + * @param ctx details to respond with + * @return MHD result code + */ +static MHD_RESULT +reply_deposit_details ( + const struct DepositWtidContext *ctx) +{ + struct MHD_Connection *connection = ctx->rc->connection; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_confirm_wire_sign ( + &TEH_keys_exchange_sign_, + &ctx->h_wire, + &ctx->h_contract_terms, + &ctx->wtid, + &ctx->coin_pub, + ctx->execution_time, + &ctx->coin_delta, + &pub, + &sig))) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("wtid", + &ctx->wtid), + GNUNET_JSON_pack_timestamp ("execution_time", + ctx->execution_time), + TALER_JSON_pack_amount ("coin_contribution", + &ctx->coin_delta), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct DepositWtidContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct DepositWtidContext *ctx = cls; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + if (GNUNET_YES != ctx->suspended) + return; /* might get multiple wake-up events */ + GNUNET_CONTAINER_DLL_remove (dwc_head, + dwc_tail, + ctx); + GNUNET_async_scope_enter (&ctx->rc->async_scope_id, + &old_scope); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming request handling\n"); + TEH_check_invariants (); + ctx->suspended = GNUNET_NO; + MHD_resume_connection (ctx->rc->connection); + TALER_MHD_daemon_trigger (); + TEH_check_invariants (); + GNUNET_async_scope_restore (&old_scope); +} + + +/** + * Lookup and return the wire transfer identifier. + * + * @param ctx context of the signed request to execute + * @return MHD result code + */ +static MHD_RESULT +handle_track_transaction_request ( + struct DepositWtidContext *ctx) +{ + struct MHD_Connection *connection = ctx->rc->connection; + enum GNUNET_DB_QueryStatus qs; + bool pending; + struct TALER_Amount fee; + + qs = TEH_plugin->lookup_transfer_by_deposit ( + TEH_plugin->cls, + &ctx->h_contract_terms, + &ctx->h_wire, + &ctx->coin_pub, + &ctx->merchant, + &pending, + &ctx->wtid, + &ctx->execution_time, + &ctx->coin_contribution, + &fee, + &ctx->kyc, + &ctx->account_pub); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_transfer_by_deposit"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_DEPOSITS_GET_NOT_FOUND, + NULL); + } + + if (0 > + TALER_amount_subtract (&ctx->coin_delta, + &ctx->coin_contribution, + &fee)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "wire fees exceed aggregate in database"); + } + if (pending) + { + if (GNUNET_TIME_absolute_is_future (ctx->timeout)) + { + bool do_suspend = false; + switch (ctx->lpt) + { + case TALER_DGLPT_NONE: + break; + case TALER_DGLPT_KYC_REQUIRED_OR_OK: + do_suspend = ctx->kyc.ok; + break; + case TALER_DGLPT_OK: + do_suspend = true; + break; + } + if (do_suspend) + { + GNUNET_assert (GNUNET_NO == ctx->suspended); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending request handling\n"); + GNUNET_CONTAINER_DLL_insert (dwc_head, + dwc_tail, + ctx); + ctx->suspended = GNUNET_YES; + MHD_suspend_connection (connection); + return MHD_YES; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC required with row %llu\n", + (unsigned long long) ctx->kyc.requirement_row); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_allow_null ( + (0 == ctx->kyc.requirement_row) + ? GNUNET_JSON_pack_string ("requirement_row", + NULL) + : GNUNET_JSON_pack_uint64 ("requirement_row", + ctx->kyc.requirement_row)), + GNUNET_JSON_pack_allow_null ( + (GNUNET_is_zero (&ctx->account_pub)) + ? GNUNET_JSON_pack_string ("account_pub", + NULL) + : GNUNET_JSON_pack_data_auto ("account_pub", + &ctx->account_pub)), + GNUNET_JSON_pack_bool ("kyc_ok", + ctx->kyc.ok), + GNUNET_JSON_pack_timestamp ("execution_time", + ctx->execution_time)); + } + return reply_deposit_details (ctx); +} + + +/** + * Function called to clean up a context. + * + * @param rc request context with data to clean up + */ +static void +dwc_cleaner (struct TEH_RequestContext *rc) +{ + struct DepositWtidContext *ctx = rc->rh_ctx; + + GNUNET_assert (GNUNET_YES != ctx->suspended); + if (NULL != ctx->eh) + { + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + ctx->eh); + ctx->eh = NULL; + } + GNUNET_free (ctx); +} + + +MHD_RESULT +TEH_handler_deposits_get (struct TEH_RequestContext *rc, + const char *const args[4]) +{ + struct DepositWtidContext *ctx = rc->rh_ctx; + + if (NULL == ctx) + { + ctx = GNUNET_new (struct DepositWtidContext); + ctx->rc = rc; + ctx->lpt = TALER_DGLPT_OK; /* default */ + rc->rh_ctx = ctx; + rc->rh_cleaner = &dwc_cleaner; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &ctx->h_wire, + sizeof (ctx->h_wire))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE, + args[0]); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[1], + strlen (args[1]), + &ctx->merchant, + sizeof (ctx->merchant))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB, + args[1]); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[2], + strlen (args[2]), + &ctx->h_contract_terms, + sizeof (ctx->h_contract_terms))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS, + args[2]); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[3], + strlen (args[3]), + &ctx->coin_pub, + sizeof (ctx->coin_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB, + args[3]); + } + TALER_MHD_parse_request_arg_auto_t (rc->connection, + "merchant_sig", + &ctx->merchant_sig); + TALER_MHD_parse_request_timeout (rc->connection, + &ctx->timeout); + { + uint64_t num = 0; + int val; + + TALER_MHD_parse_request_number (rc->connection, + "lpt", + &num); + val = (int) num; + if ( (val < 0) || + (val > TALER_DGLPT_MAX) ) + { + /* Protocol violation, but we can be graceful and + just ignore the long polling! */ + GNUNET_break_op (0); + val = TALER_DGLPT_NONE; + } + ctx->lpt = (enum TALER_DepositGetLongPollTarget) val; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Long polling for target %d with timeout %s\n", + val, + GNUNET_TIME_relative2s ( + GNUNET_TIME_absolute_get_remaining ( + ctx->timeout), + true)); + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + { + if (GNUNET_OK != + TALER_merchant_deposit_verify (&ctx->merchant, + &ctx->coin_pub, + &ctx->h_contract_terms, + &ctx->h_wire, + &ctx->merchant_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID, + NULL); + } + } + if ( (GNUNET_TIME_absolute_is_future (ctx->timeout)) && + (TALER_DGLPT_NONE != ctx->lpt) ) + { + struct TALER_CoinDepositEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_DEPOSIT_STATUS_CHANGED), + .merchant_pub = ctx->merchant + }; + + ctx->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (ctx->timeout), + &rep.header, + &db_event_cb, + ctx); + GNUNET_break (NULL != ctx->eh); + } + } + + return handle_track_transaction_request (ctx); +} + + +/* end of taler-exchange-httpd_deposits_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-deposits.h b/src/exchange/taler-exchange-httpd_get-deposits.h @@ -0,0 +1,50 @@ +/* + This file is part of TALER + Copyright (C) 2014-2017, 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-deposits.h + * @brief Handle wire transfer tracking-related requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_DEPOSITS_H +#define TALER_EXCHANGE_HTTPD_GET_DEPOSITS_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Resume long pollers on GET /deposits. + */ +void +TEH_deposits_get_cleanup (void); + + +/** + * Handle a "/deposits/$H_WIRE/$MERCHANT_PUB/$H_CONTRACT_TERMS/$COIN_PUB" + * request. + * + * @param rc request context + * @param args array of additional options (length: 4, contains: + * h_wire, merchant_pub, h_contract_terms and coin_pub) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_deposits_get (struct TEH_RequestContext *rc, + const char *const args[4]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-keys.c b/src/exchange/taler-exchange-httpd_get-keys.c @@ -0,0 +1,4428 @@ +/* + This file is part of TALER + Copyright (C) 2020-2025 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 <http://www.gnu.org/licenses/> + */ +/** + * @file taler-exchange-httpd_get-keys.c + * @brief management of our various keys + * @author Christian Grothoff + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd.h" +#include "taler-exchange-httpd_get-config.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler/taler_extensions.h" + + +/** + * How many /keys request do we hold in suspension at + * most at any time? + */ +#define SKR_LIMIT 32 + + +/** + * When do we forcefully timeout a /keys request? + * Matches the 120s hard-coded into exchange_api_handle.c + */ +#define KEYS_TIMEOUT \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 2) + + +/** + * Information about a denomination on offer by the denomination helper. + */ +struct HelperDenomination +{ + + /** + * When will the helper start to use this key for signing? + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * For how long will the helper allow signing? 0 if + * the key was revoked or purged. + */ + struct GNUNET_TIME_Relative validity_duration; + + /** + * Hash of the full denomination key. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Signature over this key from the security module's key. + */ + struct TALER_SecurityModuleSignatureP sm_sig; + + /** + * The (full) public key. + */ + struct TALER_DenominationPublicKey denom_pub; + + /** + * Details depend on the @e denom_pub.cipher type. + */ + union + { + + /** + * Hash of the RSA key. + */ + struct TALER_RsaPubHashP h_rsa; + + /** + * Hash of the CS key. + */ + struct TALER_CsPubHashP h_cs; + + } h_details; + + /** + * Name in configuration section for this denomination type. + */ + char *section_name; + + +}; + + +/** + * Signatures of an auditor over a denomination key of this exchange. + */ +struct TEH_AuditorSignature +{ + /** + * We store the signatures in a DLL. + */ + struct TEH_AuditorSignature *prev; + + /** + * We store the signatures in a DLL. + */ + struct TEH_AuditorSignature *next; + + /** + * A signature from the auditor. + */ + struct TALER_AuditorSignatureP asig; + + /** + * Public key of the auditor. + */ + struct TALER_AuditorPublicKeyP apub; + +}; + + +/** + * Information about a signing key on offer by the esign helper. + */ +struct HelperSignkey +{ + /** + * When will the helper start to use this key for signing? + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * For how long will the helper allow signing? 0 if + * the key was revoked or purged. + */ + struct GNUNET_TIME_Relative validity_duration; + + /** + * The public key. + */ + struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * Signature over this key from the security module's key. + */ + struct TALER_SecurityModuleSignatureP sm_sig; + +}; + + +/** + * State associated with the crypto helpers / security modules. NOT updated + * when the #key_generation is updated (instead constantly kept in sync + * whenever #TEH_keys_get_state() is called). + */ +struct HelperState +{ + + /** + * Handle for the esign/EdDSA helper. + */ + struct TALER_CRYPTO_ExchangeSignHelper *esh; + + /** + * Handle for the denom/RSA helper. + */ + struct TALER_CRYPTO_RsaDenominationHelper *rsadh; + + /** + * Handle for the denom/CS helper. + */ + struct TALER_CRYPTO_CsDenominationHelper *csdh; + + /** + * Map from H(denom_pub) to `struct HelperDenomination` entries. + */ + struct GNUNET_CONTAINER_MultiHashMap *denom_keys; + + /** + * Map from H(rsa_pub) to `struct HelperDenomination` entries. + */ + struct GNUNET_CONTAINER_MultiHashMap *rsa_keys; + + /** + * Map from H(cs_pub) to `struct HelperDenomination` entries. + */ + struct GNUNET_CONTAINER_MultiHashMap *cs_keys; + + /** + * Map from `struct TALER_ExchangePublicKey` to `struct HelperSignkey` + * entries. Based on the fact that a `struct GNUNET_PeerIdentity` is also + * an EdDSA public key. + */ + struct GNUNET_CONTAINER_MultiPeerMap *esign_keys; + +}; + + +/** + * Information we track for the crypto helpers. Preserved + * when the @e key_generation changes, thus kept separate. + */ +static struct HelperState helpers; + + +/** + * Entry in (sorted) array with possible pre-build responses for /keys. + * We keep pre-build responses for the various (valid) cherry-picking + * values around. + */ +struct KeysResponseData +{ + + /** + * Response to return if the client supports (deflate) compression. + */ + struct MHD_Response *response_compressed; + + /** + * Response to return if the client does not support compression. + */ + struct MHD_Response *response_uncompressed; + + /** + * ETag for these responses. + */ + char *etag; + + /** + * Cherry-picking timestamp the client must have set for this + * response to be valid. 0 if this is the "full" response. + * The client's request must include this date or a higher one + * for this response to be applicable. + */ + struct GNUNET_TIME_Timestamp cherry_pick_date; + +}; + + +/** + * @brief All information about an exchange online signing key (which is used to + * sign messages from the exchange). + */ +struct SigningKey +{ + + /** + * The exchange's (online signing) public key. + */ + struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * Meta data about the signing key, such as validity periods. + */ + struct TALER_EXCHANGEDB_SignkeyMetaData meta; + + /** + * The long-term offline master key's signature for this signing key. + * Signs over @e exchange_pub and @e meta. + */ + struct TALER_MasterSignatureP master_sig; + +}; + +struct TEH_KeyStateHandle +{ + + /** + * Mapping from denomination keys to denomination key issue struct. + * Used to lookup the key by hash. + */ + struct GNUNET_CONTAINER_MultiHashMap *denomkey_map; + + /** + * Mapping from serial ID's to denomination key issue struct. + * Used to lookup the key by serial ID. + * + * FIXME: We need a 64-bit version of this in GNUNET. + */ + struct GNUNET_CONTAINER_MultiHashMap32 *denomserial_map; + + /** + * Map from `struct TALER_ExchangePublicKey` to `struct SigningKey` + * entries. Based on the fact that a `struct GNUNET_PeerIdentity` is also + * an EdDSA public key. + */ + struct GNUNET_CONTAINER_MultiPeerMap *signkey_map; + + /** + * Head of DLL of our global fees. + */ + struct TEH_GlobalFee *gf_head; + + /** + * Tail of DLL of our global fees. + */ + struct TEH_GlobalFee *gf_tail; + + /** + * json array with the auditors of this exchange. Contains exactly + * the information needed for the "auditors" field of the /keys response. + */ + json_t *auditors; + + /** + * json array with the global fees of this exchange. Contains exactly + * the information needed for the "global_fees" field of the /keys response. + */ + json_t *global_fees; + + /** + * Sorted array of responses to /keys (MUST be sorted by cherry-picking date) of + * length @e krd_array_length; + */ + struct KeysResponseData *krd_array; + + /** + * Length of the @e krd_array. + */ + unsigned int krd_array_length; + + /** + * Cached reply for a GET /management/keys request. Used so we do not + * re-create the reply every time. + */ + json_t *management_keys_reply; + + /** + * For which (global) key_generation was this data structure created? + * Used to check when we are outdated and need to be re-generated. + */ + uint64_t key_generation; + + /** + * When did we initiate the key reloading? + */ + struct GNUNET_TIME_Timestamp reload_time; + + /** + * What is the period at which we rotate keys + * (signing or denomination keys)? + */ + struct GNUNET_TIME_Relative rekey_frequency; + + /** + * When does our online signing key expire and we + * thus need to re-generate this response? + */ + struct GNUNET_TIME_Timestamp signature_expires; + + /** + * True if #finish_keys_response() was not yet run and this key state + * is only suitable for the /management/keys API. + */ + bool management_only; + +}; + + +/** + * Entry of /keys requests that are currently suspended because we are + * waiting for /keys to become ready. + */ +struct SuspendedKeysRequests +{ + /** + * Kept in a DLL. + */ + struct SuspendedKeysRequests *next; + + /** + * Kept in a DLL. + */ + struct SuspendedKeysRequests *prev; + + /** + * The suspended connection. + */ + struct MHD_Connection *connection; + + /** + * When does this request timeout? + */ + struct GNUNET_TIME_Absolute timeout; +}; + + +/** + * Information we track about wire fees. + */ +struct WireFeeSet +{ + + /** + * Kept in a DLL. + */ + struct WireFeeSet *next; + + /** + * Kept in a DLL. + */ + struct WireFeeSet *prev; + + /** + * Actual fees. + */ + struct TALER_WireFeeSet fees; + + /** + * Start date of fee validity (inclusive). + */ + struct GNUNET_TIME_Timestamp start_date; + + /** + * End date of fee validity (exclusive). + */ + struct GNUNET_TIME_Timestamp end_date; + + /** + * Wire method the fees apply to. + */ + char *method; +}; + + +/** + * State we keep per thread to cache the wire part of the /keys response. + */ +struct WireStateHandle +{ + + /** + * JSON reply for wire response. + */ + json_t *json_reply; + + /** + * ETag for this response (if any). + */ + char *etag; + + /** + * head of DLL of wire fees. + */ + struct WireFeeSet *wfs_head; + + /** + * Tail of DLL of wire fees. + */ + struct WireFeeSet *wfs_tail; + + /** + * Earliest timestamp of all the wire methods when we have no more fees. + */ + struct GNUNET_TIME_Absolute cache_expiration; + + /** + * @e cache_expiration time, formatted. + */ + char dat[128]; + + /** + * For which (global) wire_generation was this data structure created? + * Used to check when we are outdated and need to be re-generated. + */ + uint64_t wire_generation; + + /** + * Is the wire data ready? + */ + bool ready; + +}; + + +/** + * Stores the latest generation of our wire response. + */ +static struct WireStateHandle *wire_state; + +/** + * Handler listening for wire updates by other exchange + * services. + */ +static struct GNUNET_DB_EventHandler *wire_eh; + +/** + * Counter incremented whenever we have a reason to re-build the #wire_state + * because something external changed. + */ +static uint64_t wire_generation; + + +/** + * Stores the latest generation of our key state. + */ +static struct TEH_KeyStateHandle *key_state; + +/** + * Counter incremented whenever we have a reason to re-build the keys because + * something external changed. See #TEH_keys_get_state() and + * #TEH_keys_update_states() for uses of this variable. + */ +static uint64_t key_generation; + +/** + * Handler listening for wire updates by other exchange + * services. + */ +static struct GNUNET_DB_EventHandler *keys_eh; + +/** + * Head of DLL of suspended /keys requests. + */ +static struct SuspendedKeysRequests *skr_head; + +/** + * Tail of DLL of suspended /keys requests. + */ +static struct SuspendedKeysRequests *skr_tail; + +/** + * Number of entries in the @e skr_head DLL. + */ +static unsigned int skr_size; + +/** + * Task to force timeouts on /keys requests. + */ +static struct GNUNET_SCHEDULER_Task *keys_tt; + +/** + * For how long should a signing key be legally retained? + * Configuration value. + */ +static struct GNUNET_TIME_Relative signkey_legal_duration; + +/** + * What type of asset are we dealing with here? + */ +static char *asset_type; + +/** + * RSA security module public key, all zero if not known. + */ +static struct TALER_SecurityModulePublicKeyP denom_rsa_sm_pub; + +/** + * CS security module public key, all zero if not known. + */ +static struct TALER_SecurityModulePublicKeyP denom_cs_sm_pub; + +/** + * EdDSA security module public key, all zero if not known. + */ +static struct TALER_SecurityModulePublicKeyP esign_sm_pub; + +/** + * Are we shutting down? + */ +static bool terminating; + + +/** + * Free memory associated with @a wsh + * + * @param[in] wsh wire state to destroy + */ +static void +destroy_wire_state (struct WireStateHandle *wsh) +{ + struct WireFeeSet *wfs; + + while (NULL != (wfs = wsh->wfs_head)) + { + GNUNET_CONTAINER_DLL_remove (wsh->wfs_head, + wsh->wfs_tail, + wfs); + GNUNET_free (wfs->method); + GNUNET_free (wfs); + } + json_decref (wsh->json_reply); + GNUNET_free (wsh->etag); + GNUNET_free (wsh); +} + + +/** + * Function called whenever another exchange process has updated + * the wire data in the database. + * + * @param cls NULL + * @param extra unused + * @param extra_size number of bytes in @a extra unused + */ +static void +wire_update_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + (void) cls; + (void) extra; + (void) extra_size; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received wire update event\n"); + TEH_check_invariants (); + wire_generation++; + key_generation++; + TEH_resume_keys_requests (false); +} + + +/** + * Add information about a wire account to @a cls. + * + * @param cls a `json_t *` array to expand with wire account details + * @param payto_uri the exchange bank account URI to add + * @param conversion_url URL of a conversion service, NULL if there is no conversion + * @param open_banking_gateway URL of an open banking gateway, NULL if there is none + * @param wire_transfer_gateway URL of a wire transfer gateway, NULL if there is none + * @param debit_restrictions JSON array with debit restrictions on the account + * @param credit_restrictions JSON array with credit restrictions on the account + * @param master_sig master key signature affirming that this is a bank + * account of the exchange (of purpose #TALER_SIGNATURE_MASTER_WIRE_DETAILS) + * @param bank_label label the wallet should use to display the account, can be NULL + * @param priority priority for ordering bank account labels + */ +static void +add_wire_account ( + void *cls, + const struct TALER_FullPayto payto_uri, + const char *conversion_url, + const char *open_banking_gateway, + const char *wire_transfer_gateway, + const json_t *debit_restrictions, + const json_t *credit_restrictions, + const struct TALER_MasterSignatureP *master_sig, + const char *bank_label, + int64_t priority) +{ + json_t *a = cls; + + if (GNUNET_OK != + TALER_exchange_wire_signature_check ( + payto_uri, + conversion_url, + open_banking_gateway, + wire_transfer_gateway, + debit_restrictions, + credit_restrictions, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has wire account with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + if (0 != + json_array_append_new ( + a, + GNUNET_JSON_PACK ( + TALER_JSON_pack_full_payto ( + "payto_uri", + payto_uri), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "conversion_url", + conversion_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "open_banking_gateway", + open_banking_gateway)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "wire_transfer_gateway", + wire_transfer_gateway)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "bank_label", + bank_label)), + GNUNET_JSON_pack_int64 ( + "priority", + priority), + GNUNET_JSON_pack_array_incref ( + "debit_restrictions", + (json_t *) debit_restrictions), + GNUNET_JSON_pack_array_incref ( + "credit_restrictions", + (json_t *) credit_restrictions), + GNUNET_JSON_pack_data_auto ( + "master_sig", + master_sig)))) + { + GNUNET_break (0); /* out of memory!? */ + return; + } +} + + +/** + * Closure for #add_wire_fee(). + */ +struct AddContext +{ + /** + * Wire method the fees are for. + */ + char *wire_method; + + /** + * Wire state we are building. + */ + struct WireStateHandle *wsh; + + /** + * Array to append the fee to. + */ + json_t *a; + + /** + * Set to the maximum end-date seen. + */ + struct GNUNET_TIME_Absolute max_seen; +}; + + +/** + * Add information about a wire account to @a cls. + * + * @param cls a `struct AddContext` + * @param fees the wire fees we charge + * @param start_date from when are these fees valid (start date) + * @param end_date until when are these fees valid (end date, exclusive) + * @param master_sig master key signature affirming that this is the correct + * fee (of purpose #TALER_SIGNATURE_MASTER_WIRE_FEES) + */ +static void +add_wire_fee (void *cls, + const struct TALER_WireFeeSet *fees, + struct GNUNET_TIME_Timestamp start_date, + struct GNUNET_TIME_Timestamp end_date, + const struct TALER_MasterSignatureP *master_sig) +{ + struct AddContext *ac = cls; + struct WireFeeSet *wfs; + + if (GNUNET_OK != + TALER_exchange_offline_wire_fee_verify ( + ac->wire_method, + start_date, + end_date, + fees, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has wire fee with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + ac->max_seen = GNUNET_TIME_absolute_max (ac->max_seen, + end_date.abs_time); + wfs = GNUNET_new (struct WireFeeSet); + wfs->start_date = start_date; + wfs->end_date = end_date; + wfs->fees = *fees; + wfs->method = GNUNET_strdup (ac->wire_method); + GNUNET_CONTAINER_DLL_insert (ac->wsh->wfs_head, + ac->wsh->wfs_tail, + wfs); + if (0 != + json_array_append_new ( + ac->a, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("wire_fee", + &fees->wire), + TALER_JSON_pack_amount ("closing_fee", + &fees->closing), + GNUNET_JSON_pack_timestamp ("start_date", + start_date), + GNUNET_JSON_pack_timestamp ("end_date", + end_date), + GNUNET_JSON_pack_data_auto ("sig", + master_sig)))) + { + GNUNET_break (0); /* out of memory!? */ + return; + } +} + + +/** + * Create the wire response from our database state. + * + * @return NULL on error + */ +static struct WireStateHandle * +build_wire_state (void) +{ + json_t *wire_accounts_array; + json_t *wire_fee_object; + uint64_t wg = wire_generation; /* must be obtained FIRST */ + enum GNUNET_DB_QueryStatus qs; + struct WireStateHandle *wsh; + json_t *wads; + + wsh = GNUNET_new (struct WireStateHandle); + wsh->wire_generation = wg; + wire_accounts_array = json_array (); + GNUNET_assert (NULL != wire_accounts_array); + qs = TEH_plugin->get_wire_accounts (TEH_plugin->cls, + &add_wire_account, + wire_accounts_array); + if (0 > qs) + { + GNUNET_break (0); + json_decref (wire_accounts_array); + wsh->ready = false; + return wsh; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Built wire data with %u accounts (%d)\n", + (unsigned int) json_array_size (wire_accounts_array), + (int) qs); + wire_fee_object = json_object (); + GNUNET_assert (NULL != wire_fee_object); + wsh->cache_expiration = GNUNET_TIME_UNIT_FOREVER_ABS; + { + json_t *account; + size_t index; + + json_array_foreach (wire_accounts_array, + index, + account) + { + char *wire_method; + const char *payto_uri = json_string_value (json_object_get (account, + "payto_uri")); + + GNUNET_assert (NULL != payto_uri); + wire_method = TALER_payto_get_method (payto_uri); + if (NULL == wire_method) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No wire method in `%s'\n", + payto_uri); + wsh->ready = false; + json_decref (wire_accounts_array); + json_decref (wire_fee_object); + return wsh; + } + if (NULL == json_object_get (wire_fee_object, + wire_method)) + { + struct AddContext ac = { + .wire_method = wire_method, + .wsh = wsh, + .a = json_array () + }; + + GNUNET_assert (NULL != ac.a); + qs = TEH_plugin->get_wire_fees (TEH_plugin->cls, + wire_method, + &add_wire_fee, + &ac); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ac.a); + json_decref (wire_fee_object); + json_decref (wire_accounts_array); + GNUNET_free (wire_method); + wsh->ready = false; + return wsh; + } + if (0 != json_array_size (ac.a)) + { + wsh->cache_expiration + = GNUNET_TIME_absolute_min (ac.max_seen, + wsh->cache_expiration); + GNUNET_assert (0 == + json_object_set_new (wire_fee_object, + wire_method, + ac.a)); + } + else + { + json_decref (ac.a); + } + } + GNUNET_free (wire_method); + } + } + + wads = json_array (); /* #7271 */ + GNUNET_assert (NULL != wads); + wsh->json_reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("accounts", + wire_accounts_array), + GNUNET_JSON_pack_array_steal ("wads", + wads), + GNUNET_JSON_pack_object_steal ("fees", + wire_fee_object)); + wsh->ready = true; + return wsh; +} + + +void +TEH_wire_update_state (void) +{ + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_EXCHANGE_WIRE_UPDATED), + }; + + TEH_plugin->event_notify (TEH_plugin->cls, + &es, + NULL, + 0); + wire_generation++; + key_generation++; +} + + +/** + * Return the current key state for this thread. Possibly + * re-builds the key state if we have reason to believe + * that something changed. + * + * @return NULL on error + */ +static struct WireStateHandle * +get_wire_state (void) +{ + struct WireStateHandle *old_wsh; + + old_wsh = wire_state; + if ( (NULL == old_wsh) || + (old_wsh->wire_generation < wire_generation) ) + { + struct WireStateHandle *wsh; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Rebuilding wire, generation upgrade from %llu to %llu\n", + (unsigned long long) (NULL == old_wsh) ? 0LL : + old_wsh->wire_generation, + (unsigned long long) wire_generation); + TEH_check_invariants (); + wsh = build_wire_state (); + wire_state = wsh; + if (NULL != old_wsh) + destroy_wire_state (old_wsh); + TEH_check_invariants (); + return wsh; + } + return old_wsh; +} + + +const struct TALER_WireFeeSet * +TEH_wire_fees_by_time ( + struct GNUNET_TIME_Timestamp ts, + const char *method) +{ + struct WireStateHandle *wsh = get_wire_state (); + + for (struct WireFeeSet *wfs = wsh->wfs_head; + NULL != wfs; + wfs = wfs->next) + { + if (0 != strcmp (method, + wfs->method)) + continue; + if ( (GNUNET_TIME_timestamp_cmp (wfs->start_date, + >, + ts)) || + (GNUNET_TIME_timestamp_cmp (ts, + >=, + wfs->end_date)) ) + continue; + return &wfs->fees; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No wire fees for method `%s' at %s configured\n", + method, + GNUNET_TIME_timestamp2s (ts)); + return NULL; +} + + +/** + * Function called to forcefully resume suspended keys requests. + * + * @param cls unused, NULL + */ +static void +keys_timeout_cb (void *cls) +{ + struct SuspendedKeysRequests *skr; + + (void) cls; + keys_tt = NULL; + while (NULL != (skr = skr_head)) + { + if (GNUNET_TIME_absolute_is_future (skr->timeout)) + break; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming /keys request due to timeout\n"); + GNUNET_CONTAINER_DLL_remove (skr_head, + skr_tail, + skr); + skr_size--; + MHD_resume_connection (skr->connection); + TALER_MHD_daemon_trigger (); + GNUNET_free (skr); + } + if (NULL == skr) + return; + keys_tt = GNUNET_SCHEDULER_add_at (skr->timeout, + &keys_timeout_cb, + NULL); +} + + +/** + * Suspend /keys request while we (hopefully) are waiting to be + * provisioned with key material. + * + * @param[in] connection to suspend + */ +static MHD_RESULT +suspend_request (struct MHD_Connection *connection) +{ + struct SuspendedKeysRequests *skr; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending /keys request until key material changes\n"); + if (terminating) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + "Exchange terminating"); + } + skr = GNUNET_new (struct SuspendedKeysRequests); + skr->connection = connection; + MHD_suspend_connection (connection); + GNUNET_CONTAINER_DLL_insert (skr_head, + skr_tail, + skr); + skr_size++; + skr->timeout = GNUNET_TIME_relative_to_absolute (KEYS_TIMEOUT); + if (NULL == keys_tt) + { + keys_tt = GNUNET_SCHEDULER_add_at (skr->timeout, + &keys_timeout_cb, + NULL); + } + while (skr_size > SKR_LIMIT) + { + skr = skr_tail; + GNUNET_CONTAINER_DLL_remove (skr_head, + skr_tail, + skr); + skr_size--; + MHD_resume_connection (skr->connection); + TALER_MHD_daemon_trigger (); + GNUNET_free (skr); + } + return MHD_YES; +} + + +/** + * Called on each denomination key. Checks that the key still works. + * + * @param cls NULL + * @param hc denomination hash (unused) + * @param value a `struct TEH_DenominationKey` + * @return #GNUNET_OK + */ +static enum GNUNET_GenericReturnValue +check_dk (void *cls, + const struct GNUNET_HashCode *hc, + void *value) +{ + struct TEH_DenominationKey *dk = value; + + (void) cls; + (void) hc; + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + break; + case GNUNET_CRYPTO_BSA_RSA: + GNUNET_assert (GNUNET_CRYPTO_rsa_public_key_check ( + dk->denom_pub.bsign_pub_key->details.rsa_public_key)); + return GNUNET_OK; + case GNUNET_CRYPTO_BSA_CS: + /* nothing to do for GNUNET_CRYPTO_BSA_CS */ + return GNUNET_OK; + } + GNUNET_assert (0); + return GNUNET_SYSERR; +} + + +void +TEH_check_invariants () +{ + struct TEH_KeyStateHandle *ksh; + + if (0 == TEH_check_invariants_flag) + return; + ksh = TEH_keys_get_state (); + if (NULL == ksh) + return; + GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, + &check_dk, + NULL); +} + + +void +TEH_resume_keys_requests (bool do_shutdown) +{ + struct SuspendedKeysRequests *skr; + + if (do_shutdown) + terminating = true; + while (NULL != (skr = skr_head)) + { + GNUNET_CONTAINER_DLL_remove (skr_head, + skr_tail, + skr); + skr_size--; + MHD_resume_connection (skr->connection); + TALER_MHD_daemon_trigger (); + GNUNET_free (skr); + } + GNUNET_assert (0 == skr_size); +} + + +/** + * Clear memory for responses to "/keys" in @a ksh. + * + * @param[in,out] ksh key state to update + */ +static void +clear_response_cache (struct TEH_KeyStateHandle *ksh) +{ + for (unsigned int i = 0; i<ksh->krd_array_length; i++) + { + struct KeysResponseData *krd = &ksh->krd_array[i]; + + MHD_destroy_response (krd->response_compressed); + MHD_destroy_response (krd->response_uncompressed); + GNUNET_free (krd->etag); + } + GNUNET_array_grow (ksh->krd_array, + ksh->krd_array_length, + 0); +} + + +/** + * Check that the given RSA security module's public key is the one + * we have pinned. If it does not match, we die hard. + * + * @param sm_pub RSA security module public key to check + */ +static void +check_denom_rsa_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) +{ + if (0 != + GNUNET_memcmp (sm_pub, + &denom_rsa_sm_pub)) + { + if (! GNUNET_is_zero (&denom_rsa_sm_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Our RSA security module changed its key. This must not happen.\n"); + GNUNET_assert (0); + } + denom_rsa_sm_pub = *sm_pub; /* TOFU ;-) */ + } +} + + +/** + * Check that the given CS security module's public key is the one + * we have pinned. If it does not match, we die hard. + * + * @param sm_pub RSA security module public key to check + */ +static void +check_denom_cs_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) +{ + if (0 != + GNUNET_memcmp (sm_pub, + &denom_cs_sm_pub)) + { + if (! GNUNET_is_zero (&denom_cs_sm_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Our CS security module changed its key. This must not happen.\n"); + GNUNET_assert (0); + } + denom_cs_sm_pub = *sm_pub; /* TOFU ;-) */ + } +} + + +/** + * Check that the given EdDSA security module's public key is the one + * we have pinned. If it does not match, we die hard. + * + * @param sm_pub EdDSA security module public key to check + */ +static void +check_esign_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) +{ + if (0 != + GNUNET_memcmp (sm_pub, + &esign_sm_pub)) + { + if (! GNUNET_is_zero (&esign_sm_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Our EdDSA security module changed its key. This must not happen.\n"); + GNUNET_assert (0); + } + esign_sm_pub = *sm_pub; /* TOFU ;-) */ + } +} + + +/** + * Helper function for #destroy_key_helpers to free all entries + * in the `denom_keys` map. + * + * @param cls the `struct HelperDenomination` + * @param h_denom_pub hash of the denomination public key + * @param value the `struct HelperDenomination` to release + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +free_denom_cb (void *cls, + const struct GNUNET_HashCode *h_denom_pub, + void *value) +{ + struct HelperDenomination *hd = value; + + (void) cls; + (void) h_denom_pub; + TALER_denom_pub_free (&hd->denom_pub); + GNUNET_free (hd->section_name); + GNUNET_free (hd); + return GNUNET_OK; +} + + +/** + * Helper function for #destroy_key_helpers to free all entries + * in the `esign_keys` map. + * + * @param cls the `struct HelperSignkey` + * @param pid unused, matches the exchange public key + * @param value the `struct HelperSignkey` to release + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +free_esign_cb (void *cls, + const struct GNUNET_PeerIdentity *pid, + void *value) +{ + struct HelperSignkey *hsk = value; + + (void) cls; + (void) pid; + GNUNET_free (hsk); + return GNUNET_OK; +} + + +/** + * Destroy helper state. Does NOT call free() on @a hs, as that + * state is not separately allocated! Dual to #setup_key_helpers(). + * + * @param[in] hs helper state to free, but NOT the @a hs pointer itself! + */ +static void +destroy_key_helpers (struct HelperState *hs) +{ + GNUNET_CONTAINER_multihashmap_iterate (hs->denom_keys, + &free_denom_cb, + hs); + GNUNET_CONTAINER_multihashmap_destroy (hs->rsa_keys); + hs->rsa_keys = NULL; + GNUNET_CONTAINER_multihashmap_destroy (hs->cs_keys); + hs->cs_keys = NULL; + GNUNET_CONTAINER_multihashmap_destroy (hs->denom_keys); + hs->denom_keys = NULL; + GNUNET_CONTAINER_multipeermap_iterate (hs->esign_keys, + &free_esign_cb, + hs); + GNUNET_CONTAINER_multipeermap_destroy (hs->esign_keys); + hs->esign_keys = NULL; + if (NULL != hs->rsadh) + { + TALER_CRYPTO_helper_rsa_disconnect (hs->rsadh); + hs->rsadh = NULL; + } + if (NULL != hs->csdh) + { + TALER_CRYPTO_helper_cs_disconnect (hs->csdh); + hs->csdh = NULL; + } + if (NULL != hs->esh) + { + TALER_CRYPTO_helper_esign_disconnect (hs->esh); + hs->esh = NULL; + } +} + + +/** + * Looks up the AGE_RESTRICTED setting for a denomination in the config and + * returns the age restriction (mask) accordingly. + * + * @param section_name Section in the configuration for the particular + * denomination. + */ +static struct TALER_AgeMask +load_age_mask (const char *section_name) +{ + static const struct TALER_AgeMask null_mask = {0}; + enum GNUNET_GenericReturnValue ret; + + if (GNUNET_OK != (GNUNET_CONFIGURATION_have_value ( + TEH_cfg, + section_name, + "AGE_RESTRICTED"))) + return null_mask; + + if (GNUNET_SYSERR == + (ret = GNUNET_CONFIGURATION_get_value_yesno (TEH_cfg, + section_name, + "AGE_RESTRICTED"))) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section_name, + "AGE_RESTRICTED", + "Value must be YES or NO\n"); + return null_mask; + } + + if (GNUNET_OK == ret) + { + if (! TEH_age_restriction_enabled) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "age restriction set in section %s, yet, age restriction is not enabled\n", + section_name); + return TEH_age_restriction_config.mask; + } + + + return null_mask; +} + + +/** + * Function called with information about available keys for signing. Usually + * only called once per key upon connect. Also called again in case a key is + * being revoked, in that case with an @a end_time of zero. + * + * @param cls closure with the `struct HelperState *` + * @param section_name name of the denomination type in the configuration; + * NULL if the key has been revoked or purged + * @param start_time when does the key become available for signing; + * zero if the key has been revoked or purged + * @param validity_duration how long does the key remain available for signing; + * zero if the key has been revoked or purged + * @param h_rsa hash of the @a denom_pub that is available (or was purged) + * @param bs_pub the public key itself, NULL if the key was revoked or purged + * @param sm_pub public key of the security module, NULL if the key was revoked or purged + * @param sm_sig signature from the security module + */ +static void +helper_rsa_cb ( + void *cls, + const char *section_name, + struct GNUNET_TIME_Timestamp start_time, + struct GNUNET_TIME_Relative validity_duration, + const struct TALER_RsaPubHashP *h_rsa, + struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub, + const struct TALER_SecurityModulePublicKeyP *sm_pub, + const struct TALER_SecurityModuleSignatureP *sm_sig) +{ + struct HelperState *hs = cls; + struct HelperDenomination *hd; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "RSA helper announces key %s for denomination type %s with validity %s\n", + GNUNET_h2s (&h_rsa->hash), + section_name, + GNUNET_STRINGS_relative_time_to_string (validity_duration, + GNUNET_NO)); + key_generation++; + TEH_resume_keys_requests (false); + hd = GNUNET_CONTAINER_multihashmap_get (hs->rsa_keys, + &h_rsa->hash); + if (NULL != hd) + { + /* should be just an update (revocation!), so update existing entry */ + hd->validity_duration = validity_duration; + return; + } + GNUNET_assert (NULL != sm_pub); + check_denom_rsa_sm_pub (sm_pub); + hd = GNUNET_new (struct HelperDenomination); + hd->start_time = start_time; + hd->validity_duration = validity_duration; + hd->h_details.h_rsa = *h_rsa; + hd->sm_sig = *sm_sig; + GNUNET_assert (GNUNET_CRYPTO_BSA_RSA == bs_pub->cipher); + hd->denom_pub.bsign_pub_key = + GNUNET_CRYPTO_bsign_pub_incref (bs_pub); + /* load the age mask for the denomination, if applicable */ + hd->denom_pub.age_mask = load_age_mask (section_name); + TALER_denom_pub_hash (&hd->denom_pub, + &hd->h_denom_pub); + hd->section_name = GNUNET_strdup (section_name); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + hs->denom_keys, + &hd->h_denom_pub.hash, + hd, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + hs->rsa_keys, + &hd->h_details.h_rsa.hash, + hd, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); +} + + +/** + * Function called with information about available CS keys for signing. Usually + * only called once per key upon connect. Also called again in case a key is + * being revoked, in that case with an @a end_time of zero. + * + * @param cls closure with the `struct HelperState *` + * @param section_name name of the denomination type in the configuration; + * NULL if the key has been revoked or purged + * @param start_time when does the key become available for signing; + * zero if the key has been revoked or purged + * @param validity_duration how long does the key remain available for signing; + * zero if the key has been revoked or purged + * @param h_cs hash of the @a denom_pub that is available (or was purged) + * @param bs_pub the public key itself, NULL if the key was revoked or purged + * @param sm_pub public key of the security module, NULL if the key was revoked or purged + * @param sm_sig signature from the security module + */ +static void +helper_cs_cb ( + void *cls, + const char *section_name, + struct GNUNET_TIME_Timestamp start_time, + struct GNUNET_TIME_Relative validity_duration, + const struct TALER_CsPubHashP *h_cs, + struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub, + const struct TALER_SecurityModulePublicKeyP *sm_pub, + const struct TALER_SecurityModuleSignatureP *sm_sig) +{ + struct HelperState *hs = cls; + struct HelperDenomination *hd; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "CS helper announces key %s for denomination type %s with validity %s\n", + GNUNET_h2s (&h_cs->hash), + section_name, + GNUNET_STRINGS_relative_time_to_string (validity_duration, + GNUNET_NO)); + key_generation++; + TEH_resume_keys_requests (false); + hd = GNUNET_CONTAINER_multihashmap_get (hs->cs_keys, + &h_cs->hash); + if (NULL != hd) + { + /* should be just an update (revocation!), so update existing entry */ + hd->validity_duration = validity_duration; + return; + } + GNUNET_assert (NULL != sm_pub); + check_denom_cs_sm_pub (sm_pub); + hd = GNUNET_new (struct HelperDenomination); + hd->start_time = start_time; + hd->validity_duration = validity_duration; + hd->h_details.h_cs = *h_cs; + hd->sm_sig = *sm_sig; + GNUNET_assert (GNUNET_CRYPTO_BSA_CS == bs_pub->cipher); + hd->denom_pub.bsign_pub_key + = GNUNET_CRYPTO_bsign_pub_incref (bs_pub); + /* load the age mask for the denomination, if applicable */ + hd->denom_pub.age_mask = load_age_mask (section_name); + TALER_denom_pub_hash (&hd->denom_pub, + &hd->h_denom_pub); + hd->section_name = GNUNET_strdup (section_name); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + hs->denom_keys, + &hd->h_denom_pub.hash, + hd, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + hs->cs_keys, + &hd->h_details.h_cs.hash, + hd, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); +} + + +/** + * Function called with information about available keys for signing. Usually + * only called once per key upon connect. Also called again in case a key is + * being revoked, in that case with an @a end_time of zero. + * + * @param cls closure with the `struct HelperState *` + * @param start_time when does the key become available for signing; + * zero if the key has been revoked or purged + * @param validity_duration how long does the key remain available for signing; + * zero if the key has been revoked or purged + * @param exchange_pub the public key itself, NULL if the key was revoked or purged + * @param sm_pub public key of the security module, NULL if the key was revoked or purged + * @param sm_sig signature from the security module + */ +static void +helper_esign_cb ( + void *cls, + struct GNUNET_TIME_Timestamp start_time, + struct GNUNET_TIME_Relative validity_duration, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_SecurityModulePublicKeyP *sm_pub, + const struct TALER_SecurityModuleSignatureP *sm_sig) +{ + struct HelperState *hs = cls; + struct HelperSignkey *hsk; + struct GNUNET_PeerIdentity pid; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "EdDSA helper announces signing key %s with validity %s\n", + TALER_B2S (exchange_pub), + GNUNET_STRINGS_relative_time_to_string (validity_duration, + GNUNET_NO)); + key_generation++; + TEH_resume_keys_requests (false); + pid.public_key = exchange_pub->eddsa_pub; + hsk = GNUNET_CONTAINER_multipeermap_get (hs->esign_keys, + &pid); + if (NULL != hsk) + { + /* should be just an update (revocation!), so update existing entry */ + hsk->validity_duration = validity_duration; + return; + } + GNUNET_assert (NULL != sm_pub); + check_esign_sm_pub (sm_pub); + hsk = GNUNET_new (struct HelperSignkey); + hsk->start_time = start_time; + hsk->validity_duration = validity_duration; + hsk->exchange_pub = *exchange_pub; + hsk->sm_sig = *sm_sig; + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multipeermap_put ( + hs->esign_keys, + &pid, + hsk, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); +} + + +/** + * Setup helper state. + * + * @param[out] hs helper state to initialize + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +setup_key_helpers (struct HelperState *hs) +{ + hs->denom_keys + = GNUNET_CONTAINER_multihashmap_create (1024, + GNUNET_YES); + hs->rsa_keys + = GNUNET_CONTAINER_multihashmap_create (1024, + GNUNET_YES); + hs->cs_keys + = GNUNET_CONTAINER_multihashmap_create (1024, + GNUNET_YES); + hs->esign_keys + = GNUNET_CONTAINER_multipeermap_create (32, + GNUNET_NO /* MUST BE NO! */); + hs->rsadh = TALER_CRYPTO_helper_rsa_connect (TEH_cfg, + "taler-exchange", + &helper_rsa_cb, + hs); + if (NULL == hs->rsadh) + return GNUNET_SYSERR; + hs->csdh = TALER_CRYPTO_helper_cs_connect (TEH_cfg, + "taler-exchange", + &helper_cs_cb, + hs); + if (NULL == hs->csdh) + return GNUNET_SYSERR; + hs->esh = TALER_CRYPTO_helper_esign_connect (TEH_cfg, + "taler-exchange", + &helper_esign_cb, + hs); + if (NULL == hs->esh) + return GNUNET_SYSERR; + return GNUNET_OK; +} + + +/** + * Synchronize helper state. Polls the key helper for updates. + * + * @param[in,out] hs helper state to synchronize + */ +static void +sync_key_helpers (struct HelperState *hs) +{ + TALER_CRYPTO_helper_rsa_poll (hs->rsadh); + TALER_CRYPTO_helper_cs_poll (hs->csdh); + TALER_CRYPTO_helper_esign_poll (hs->esh); +} + + +/** + * Free denomination key data. + * + * @param cls a `struct TEH_KeyStateHandle`, unused + * @param h_denom_pub hash of the denomination public key, unused + * @param value a `struct TEH_DenominationKey` to free + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +clear_denomination_cb (void *cls, + const struct GNUNET_HashCode *h_denom_pub, + void *value) +{ + struct TEH_DenominationKey *dk = value; + struct TEH_AuditorSignature *as; + + (void) cls; + (void) h_denom_pub; + TALER_denom_pub_free (&dk->denom_pub); + while (NULL != (as = dk->as_head)) + { + GNUNET_CONTAINER_DLL_remove (dk->as_head, + dk->as_tail, + as); + GNUNET_free (as); + } + GNUNET_free (dk); + return GNUNET_OK; +} + + +/** + * Free denomination key data. + * + * @param cls a `struct TEH_KeyStateHandle`, unused + * @param pid the online signing key (type-disguised), unused + * @param value a `struct SigningKey` to free + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +clear_signkey_cb (void *cls, + const struct GNUNET_PeerIdentity *pid, + void *value) +{ + struct SigningKey *sk = value; + + (void) cls; + (void) pid; + GNUNET_free (sk); + return GNUNET_OK; +} + + +/** + * Free resources associated with @a cls, possibly excluding + * the helper data. + * + * @param[in] ksh key state to release + */ +static void +destroy_key_state (struct TEH_KeyStateHandle *ksh) +{ + struct TEH_GlobalFee *gf; + + clear_response_cache (ksh); + while (NULL != (gf = ksh->gf_head)) + { + GNUNET_CONTAINER_DLL_remove (ksh->gf_head, + ksh->gf_tail, + gf); + GNUNET_free (gf); + } + GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, + &clear_denomination_cb, + ksh); + GNUNET_CONTAINER_multihashmap_destroy (ksh->denomkey_map); + GNUNET_CONTAINER_multihashmap32_destroy (ksh->denomserial_map); + GNUNET_CONTAINER_multipeermap_iterate (ksh->signkey_map, + &clear_signkey_cb, + ksh); + GNUNET_CONTAINER_multipeermap_destroy (ksh->signkey_map); + json_decref (ksh->auditors); + ksh->auditors = NULL; + json_decref (ksh->global_fees); + ksh->global_fees = NULL; + if (NULL != ksh->management_keys_reply) + { + json_decref (ksh->management_keys_reply); + ksh->management_keys_reply = NULL; + } + GNUNET_free (ksh); +} + + +/** + * Function called whenever another exchange process has updated + * the keys data in the database. + * + * @param cls NULL + * @param extra unused + * @param extra_size number of bytes in @a extra unused + */ +static void +keys_update_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + (void) cls; + (void) extra; + (void) extra_size; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received /keys update event\n"); + TEH_check_invariants (); + key_generation++; + TEH_resume_keys_requests (false); + TEH_check_invariants (); +} + + +enum GNUNET_GenericReturnValue +TEH_keys_init () +{ + if (GNUNET_OK != + setup_key_helpers (&helpers)) + { + destroy_key_helpers (&helpers); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (TEH_cfg, + "exchange", + "SIGNKEY_LEGAL_DURATION", + &signkey_legal_duration)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "SIGNKEY_LEGAL_DURATION"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (TEH_cfg, + "exchange", + "ASSET_TYPE", + &asset_type)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "exchange", + "ASSET_TYPE"); + asset_type = GNUNET_strdup ("fiat"); + } + { + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), + }; + + keys_eh = TEH_plugin->event_listen (TEH_plugin->cls, + GNUNET_TIME_UNIT_FOREVER_REL, + &es, + &keys_update_event_cb, + NULL); + if (NULL == keys_eh) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + { + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), + }; + + wire_eh = TEH_plugin->event_listen (TEH_plugin->cls, + GNUNET_TIME_UNIT_FOREVER_REL, + &es, + &wire_update_event_cb, + NULL); + if (NULL == wire_eh) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Fully clean up our state. + */ +void +TEH_keys_finished () +{ + if (NULL != wire_state) + { + destroy_wire_state (wire_state); + wire_state = NULL; + } + if (NULL != wire_eh) + { + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + wire_eh); + wire_eh = NULL; + } + if (NULL != keys_tt) + { + GNUNET_SCHEDULER_cancel (keys_tt); + keys_tt = NULL; + } + if (NULL != key_state) + destroy_key_state (key_state); + if (NULL != keys_eh) + { + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + keys_eh); + keys_eh = NULL; + } + destroy_key_helpers (&helpers); +} + + +/** + * Function called with information about the exchange's denomination keys. + * + * @param cls closure with a `struct TEH_KeyStateHandle *` + * @param denom_pub public key of the denomination + * @param h_denom_pub hash of @a denom_pub + * @param meta meta data information about the denomination type (value, expirations, fees) + * @param master_sig master signature affirming the validity of this denomination + * @param recoup_possible true if the key was revoked and clients can currently recoup + * coins of this denomination + */ +static void +denomination_info_cb ( + void *cls, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_DenominationHashP *h_denom_pub, + const struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta, + const struct TALER_MasterSignatureP *master_sig, + bool recoup_possible) +{ + struct TEH_KeyStateHandle *ksh = cls; + struct TEH_DenominationKey *dk; + + if (GNUNET_TIME_absolute_is_past (meta->expire_deposit.abs_time)) + { + /* should have been filtered by DB query already! */ + GNUNET_break (0); + return; + } + if (GNUNET_OK != + TALER_exchange_offline_denom_validity_verify ( + h_denom_pub, + meta->start, + meta->expire_withdraw, + meta->expire_deposit, + meta->expire_legal, + &meta->value, + &meta->fees, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has denomination with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + + GNUNET_assert (GNUNET_CRYPTO_BSA_INVALID != + denom_pub->bsign_pub_key->cipher); + if (GNUNET_TIME_absolute_is_zero (meta->start.abs_time) || + GNUNET_TIME_absolute_is_zero (meta->expire_withdraw.abs_time) || + GNUNET_TIME_absolute_is_zero (meta->expire_deposit.abs_time) || + GNUNET_TIME_absolute_is_zero (meta->expire_legal.abs_time) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database contains invalid denomination key %s\n", + GNUNET_h2s (&h_denom_pub->hash)); + return; + } + dk = GNUNET_new (struct TEH_DenominationKey); + TALER_denom_pub_copy (&dk->denom_pub, + denom_pub); + dk->h_denom_pub = *h_denom_pub; + dk->meta = *meta; + dk->master_sig = *master_sig; + dk->recoup_possible = recoup_possible; + dk->denom_pub.age_mask = meta->age_mask; + + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (ksh->denomkey_map, + &dk->h_denom_pub.hash, + dk, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + { + uint32_t serial32 = (uint32_t) dk->meta.serial; + + GNUNET_assert (dk->meta.serial == (uint64_t) serial32); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap32_put (ksh->denomserial_map, + serial32, + dk, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } +} + + +/** + * Function called with information about the exchange's online signing keys. + * + * @param cls closure with a `struct TEH_KeyStateHandle *` + * @param exchange_pub the public key + * @param meta meta data information about the denomination type (expirations) + * @param master_sig master signature affirming the validity of this denomination + */ +static void +signkey_info_cb ( + void *cls, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_EXCHANGEDB_SignkeyMetaData *meta, + const struct TALER_MasterSignatureP *master_sig) +{ + struct TEH_KeyStateHandle *ksh = cls; + struct SigningKey *sk; + struct GNUNET_PeerIdentity pid; + + if (GNUNET_OK != + TALER_exchange_offline_signkey_validity_verify ( + exchange_pub, + meta->start, + meta->expire_sign, + meta->expire_legal, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has signing key with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + sk = GNUNET_new (struct SigningKey); + sk->exchange_pub = *exchange_pub; + sk->meta = *meta; + sk->master_sig = *master_sig; + pid.public_key = exchange_pub->eddsa_pub; + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multipeermap_put (ksh->signkey_map, + &pid, + sk, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); +} + + +/** + * Closure for #get_auditor_sigs. + */ +struct GetAuditorSigsContext +{ + /** + * Where to store the matching signatures. + */ + json_t *denom_keys; + + /** + * Public key of the auditor to match against. + */ + const struct TALER_AuditorPublicKeyP *auditor_pub; +}; + + +/** + * Extract the auditor signatures matching the auditor's public + * key from the @a value and generate the respective JSON. + * + * @param cls a `struct GetAuditorSigsContext` + * @param h_denom_pub hash of the denomination public key + * @param value a `struct TEH_DenominationKey` + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +get_auditor_sigs (void *cls, + const struct GNUNET_HashCode *h_denom_pub, + void *value) +{ + struct GetAuditorSigsContext *ctx = cls; + struct TEH_DenominationKey *dk = value; + + for (struct TEH_AuditorSignature *as = dk->as_head; + NULL != as; + as = as->next) + { + if (0 != + GNUNET_memcmp (ctx->auditor_pub, + &as->apub)) + continue; + GNUNET_break (0 == + json_array_append_new ( + ctx->denom_keys, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("denom_pub_h", + h_denom_pub), + GNUNET_JSON_pack_data_auto ("auditor_sig", + &as->asig)))); + } + return GNUNET_OK; +} + + +/** + * Function called with information about the exchange's auditors. + * + * @param cls closure with a `struct TEH_KeyStateHandle *` + * @param auditor_pub the public key of the auditor + * @param auditor_url URL of the REST API of the auditor + * @param auditor_name human readable official name of the auditor + */ +static void +auditor_info_cb ( + void *cls, + const struct TALER_AuditorPublicKeyP *auditor_pub, + const char *auditor_url, + const char *auditor_name) +{ + struct TEH_KeyStateHandle *ksh = cls; + struct GetAuditorSigsContext ctx; + + ctx.denom_keys = json_array (); + GNUNET_assert (NULL != ctx.denom_keys); + ctx.auditor_pub = auditor_pub; + GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, + &get_auditor_sigs, + &ctx); + GNUNET_break (0 == + json_array_append_new ( + ksh->auditors, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("auditor_name", + auditor_name), + GNUNET_JSON_pack_data_auto ("auditor_pub", + auditor_pub), + GNUNET_JSON_pack_string ("auditor_url", + auditor_url), + GNUNET_JSON_pack_array_steal ("denomination_keys", + ctx.denom_keys)))); +} + + +/** + * Function called with information about the denominations + * audited by the exchange's auditors. + * + * @param cls closure with a `struct TEH_KeyStateHandle *` + * @param auditor_pub the public key of an auditor + * @param h_denom_pub hash of a denomination key audited by this auditor + * @param auditor_sig signature from the auditor affirming this + */ +static void +auditor_denom_cb ( + void *cls, + const struct TALER_AuditorPublicKeyP *auditor_pub, + const struct TALER_DenominationHashP *h_denom_pub, + const struct TALER_AuditorSignatureP *auditor_sig) +{ + struct TEH_KeyStateHandle *ksh = cls; + struct TEH_DenominationKey *dk; + struct TEH_AuditorSignature *as; + + dk = GNUNET_CONTAINER_multihashmap_get (ksh->denomkey_map, + &h_denom_pub->hash); + if (NULL == dk) + { + /* Odd, this should be impossible as per foreign key + constraint on 'auditor_denom_sigs'! Well, we can + safely continue anyway, so let's just log it. */ + GNUNET_break (0); + return; + } + as = GNUNET_new (struct TEH_AuditorSignature); + as->asig = *auditor_sig; + as->apub = *auditor_pub; + GNUNET_CONTAINER_DLL_insert (dk->as_head, + dk->as_tail, + as); +} + + +/** + * Closure for #add_sign_key_cb. + */ +struct SignKeyCtx +{ + /** + * What is the current rotation frequency for signing keys. Updated. + */ + struct GNUNET_TIME_Relative min_sk_frequency; + + /** + * JSON array of signing keys (being created). + */ + json_t *signkeys; +}; + + +/** + * Function called for all signing keys, used to build up the + * respective JSON response. + * + * @param cls a `struct SignKeyCtx *` with the array to append keys to + * @param pid the exchange public key (in type disguise) + * @param value a `struct SigningKey` + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +add_sign_key_cb (void *cls, + const struct GNUNET_PeerIdentity *pid, + void *value) +{ + struct SignKeyCtx *ctx = cls; + struct SigningKey *sk = value; + + (void) pid; + if (GNUNET_TIME_absolute_is_future (sk->meta.expire_sign.abs_time)) + { + ctx->min_sk_frequency = + GNUNET_TIME_relative_min (ctx->min_sk_frequency, + GNUNET_TIME_absolute_get_difference ( + sk->meta.start.abs_time, + sk->meta.expire_sign.abs_time)); + } + GNUNET_assert ( + 0 == + json_array_append_new ( + ctx->signkeys, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("stamp_start", + sk->meta.start), + GNUNET_JSON_pack_timestamp ("stamp_expire", + sk->meta.expire_sign), + GNUNET_JSON_pack_timestamp ("stamp_end", + sk->meta.expire_legal), + GNUNET_JSON_pack_data_auto ("master_sig", + &sk->master_sig), + GNUNET_JSON_pack_data_auto ("key", + &sk->exchange_pub)))); + return GNUNET_OK; +} + + +/** + * Closure for #add_denom_key_cb. + */ +struct DenomKeyCtx +{ + /** + * Heap for sorting active denomination keys by start time. + */ + struct GNUNET_CONTAINER_Heap *heap; + + /** + * JSON array of revoked denomination keys. + */ + json_t *recoup; + + /** + * What is the minimum key rotation frequency of + * valid denomination keys? + */ + struct GNUNET_TIME_Relative min_dk_frequency; +}; + + +/** + * Function called for all denomination keys, used to build up the + * JSON list of *revoked* denomination keys and the + * heap of non-revoked denomination keys by timeout. + * + * @param cls a `struct DenomKeyCtx` + * @param h_denom_pub hash of the denomination key + * @param value a `struct TEH_DenominationKey` + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +add_denom_key_cb (void *cls, + const struct GNUNET_HashCode *h_denom_pub, + void *value) +{ + struct DenomKeyCtx *dkc = cls; + struct TEH_DenominationKey *dk = value; + + if (dk->recoup_possible) + { + GNUNET_assert ( + 0 == + json_array_append_new ( + dkc->recoup, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_denom_pub", + h_denom_pub)))); + } + else + { + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + dkc->min_dk_frequency = + GNUNET_TIME_relative_min (dkc->min_dk_frequency, + GNUNET_TIME_absolute_get_difference ( + dk->meta.start.abs_time, + dk->meta.expire_withdraw.abs_time)); + } + (void) GNUNET_CONTAINER_heap_insert (dkc->heap, + dk, + dk->meta.start.abs_time.abs_value_us); + } + return GNUNET_OK; +} + + +/** + * Add the headers we want to set for every /keys response. + * + * @param cls the key state to use + * @param[in,out] response the response to modify + */ +static void +setup_general_response_headers (void *cls, + struct MHD_Response *response) +{ + struct TEH_KeyStateHandle *ksh = cls; + char dat[128]; + + TALER_MHD_add_global_headers (response, + true); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + "application/json")); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "public,must-revalidate,max-age=86400") + ); + if (! GNUNET_TIME_relative_is_zero (ksh->rekey_frequency)) + { + struct GNUNET_TIME_Relative r; + struct GNUNET_TIME_Absolute a; + struct GNUNET_TIME_Timestamp km; + struct GNUNET_TIME_Timestamp m; + struct GNUNET_TIME_Timestamp we; + + r = GNUNET_TIME_relative_min (TEH_max_keys_caching, + ksh->rekey_frequency); + a = GNUNET_TIME_relative_to_absolute (r); + /* Round up to next full day to ensure the expiration + time does not become a fingerprint! */ + a = GNUNET_TIME_absolute_round_down (a, + GNUNET_TIME_UNIT_DAYS); + a = GNUNET_TIME_absolute_add (a, + GNUNET_TIME_UNIT_DAYS); + km = GNUNET_TIME_absolute_to_timestamp (a); + we = GNUNET_TIME_absolute_to_timestamp (wire_state->cache_expiration); + m = GNUNET_TIME_timestamp_min (we, + km); + TALER_MHD_get_date_string (m.abs_time, + dat); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Setting /keys 'Expires' header to '%s' (rekey frequency is %s)\n", + dat, + GNUNET_TIME_relative2s (ksh->rekey_frequency, + false)); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_EXPIRES, + dat)); + ksh->signature_expires + = GNUNET_TIME_timestamp_min (m, + ksh->signature_expires); + } + /* Set cache control headers: our response varies depending on these headers */ + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_VARY, + MHD_HTTP_HEADER_ACCEPT_ENCODING)); +} + + +/** + * Initialize @a krd using the given values for @a signkeys, + * @a recoup and @a denoms. + * + * @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_cherry_pick_date timestamp to use + * @param[in,out] signkeys list of sign keys to return + * @param[in,out] recoup list of revoked keys to return + * @param[in,out] grouped_denominations list of grouped denominations to return + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +create_krd (struct TEH_KeyStateHandle *ksh, + const struct GNUNET_HashCode *denom_keys_hash, + struct GNUNET_TIME_Timestamp last_cherry_pick_date, + json_t *signkeys, + json_t *recoup, + json_t *grouped_denominations) +{ + struct KeysResponseData krd; + struct TALER_ExchangePublicKeyP exchange_pub; + struct TALER_ExchangeSignatureP exchange_sig; + struct WireStateHandle *wsh; + json_t *keys; + + wsh = get_wire_state (); + if (! wsh->ready) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_assert (! GNUNET_TIME_absolute_is_zero ( + last_cherry_pick_date.abs_time)); + GNUNET_assert (NULL != signkeys); + GNUNET_assert (NULL != recoup); + GNUNET_assert (NULL != grouped_denominations); + GNUNET_assert (NULL != ksh->auditors); + GNUNET_assert (NULL != TEH_currency); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Creating /keys at cherry pick date %s\n", + GNUNET_TIME_timestamp2s (last_cherry_pick_date)); + + /* Sign hash over master signatures of all denomination keys until this time + (in reverse order). */ + { + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = + TALER_exchange_online_key_set_sign ( + &TEH_keys_exchange_sign2_, + ksh, + last_cherry_pick_date, + denom_keys_hash, + &exchange_pub, + &exchange_sig))) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Could not create key response data: cannot sign (%s)\n", + TALER_ErrorCode_get_hint (ec)); + return GNUNET_SYSERR; + } + } + + { + const struct SigningKey *sk; + + sk = GNUNET_CONTAINER_multipeermap_get ( + ksh->signkey_map, + (const struct GNUNET_PeerIdentity *) &exchange_pub); + ksh->signature_expires = GNUNET_TIME_timestamp_min (sk->meta.expire_sign, + ksh->signature_expires); + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Build /keys data with %u wire accounts\n", + (unsigned int) json_array_size ( + json_object_get (wsh->json_reply, + "accounts"))); + + keys = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("version", + EXCHANGE_PROTOCOL_VERSION), + GNUNET_JSON_pack_string ("base_url", + TEH_base_url), + GNUNET_JSON_pack_string ("currency", + TEH_currency), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "bank_compliance_language", + TEH_bank_compliance_language)), + GNUNET_JSON_pack_object_steal ( + "currency_specification", + TALER_JSON_currency_specs_to_json (TEH_cspec)), + GNUNET_JSON_pack_array_incref ( + "hard_limits", + TEH_hard_limits), + GNUNET_JSON_pack_array_incref ( + "zero_limits", + TEH_zero_limits), + TALER_JSON_pack_amount ("stefan_abs", + &TEH_stefan_abs), + TALER_JSON_pack_amount ("stefan_log", + &TEH_stefan_log), + GNUNET_JSON_pack_double ("stefan_lin", + (double) TEH_stefan_lin), + GNUNET_JSON_pack_string ("asset_type", + asset_type), + GNUNET_JSON_pack_bool ("kyc_enabled", + GNUNET_YES == TEH_enable_kyc), + GNUNET_JSON_pack_bool ("disable_direct_deposit", + GNUNET_YES == TEH_disable_direct_deposit), + GNUNET_JSON_pack_bool ("rewards_allowed", + false), + GNUNET_JSON_pack_data_auto ("master_public_key", + &TEH_master_public_key), + GNUNET_JSON_pack_time_rel ("reserve_closing_delay", + TEH_reserve_closing_delay), + GNUNET_JSON_pack_array_incref ("signkeys", + signkeys), + GNUNET_JSON_pack_array_incref ("recoup", + recoup), + GNUNET_JSON_pack_array_incref ("wads", + json_object_get (wsh->json_reply, + "wads")), + GNUNET_JSON_pack_array_incref ("accounts", + json_object_get (wsh->json_reply, + "accounts")), + GNUNET_JSON_pack_object_incref ("wire_fees", + json_object_get (wsh->json_reply, + "fees")), + GNUNET_JSON_pack_array_incref ("denominations", + grouped_denominations), + GNUNET_JSON_pack_array_incref ("auditors", + ksh->auditors), + GNUNET_JSON_pack_array_incref ("global_fees", + ksh->global_fees), + GNUNET_JSON_pack_timestamp ("list_issue_date", + last_cherry_pick_date), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ( + "wallet_balance_limit_without_kyc", + TALER_KYCLOGIC_get_wallet_thresholds ())), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("shopping_url", + TEH_shopping_url)), + TALER_JSON_pack_amount ("tiny_amount", + &TEH_tiny_amount), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &exchange_sig)); + GNUNET_assert (NULL != keys); + /* Signal support for the configured, enabled extensions. */ + { + json_t *extensions = json_object (); + bool has_extensions = false; + + GNUNET_assert (NULL != extensions); + /* Fill in the configurations of the enabled extensions */ + for (const struct TALER_Extensions *iter = TALER_extensions_get_head (); + NULL != iter && NULL != iter->extension; + iter = iter->next) + { + const struct TALER_Extension *extension = iter->extension; + json_t *manifest; + int r; + + /* skip if not enabled */ + if (! extension->enabled) + continue; + + /* flag our findings so far */ + has_extensions = true; + + + manifest = extension->manifest (extension); + GNUNET_assert (manifest); + + r = json_object_set_new ( + extensions, + extension->name, + manifest); + GNUNET_assert (0 == r); + } + + /* Update the keys object with the extensions and its signature */ + if (has_extensions) + { + json_t *sig; + int r; + + r = json_object_set_new ( + keys, + "extensions", + extensions); + GNUNET_assert (0 == r); + + /* Add the signature of the extensions, if it is not zero */ + if (TEH_extensions_signed) + { + sig = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("extensions_sig", + &TEH_extensions_sig)); + + r = json_object_update (keys, sig); + GNUNET_assert (0 == r); + } + } + else + { + json_decref (extensions); + } + } + + + { + char *keys_json; + void *keys_jsonz; + size_t keys_jsonz_size; + int comp; + char etag[sizeof (struct GNUNET_HashCode) * 2]; + + /* Convert /keys response to UTF8-String */ + keys_json = json_dumps (keys, + JSON_INDENT (2)); + json_decref (keys); + GNUNET_assert (NULL != keys_json); + + /* Keep copy for later compression... */ + keys_jsonz = GNUNET_strdup (keys_json); + keys_jsonz_size = strlen (keys_json); + + /* hash to compute etag */ + { + struct GNUNET_HashCode ehash; + char *end; + + GNUNET_CRYPTO_hash (keys_jsonz, + keys_jsonz_size, + &ehash); + end = GNUNET_STRINGS_data_to_string (&ehash, + sizeof (ehash), + etag, + sizeof (etag)); + *end = '\0'; + } + + /* Create uncompressed response */ + krd.response_uncompressed + = MHD_create_response_from_buffer (keys_jsonz_size, + keys_json, + MHD_RESPMEM_MUST_FREE); + GNUNET_assert (NULL != krd.response_uncompressed); + setup_general_response_headers (ksh, + krd.response_uncompressed); + /* Information is always public, revalidate after 1 day */ + GNUNET_break (MHD_YES == + MHD_add_response_header (krd.response_uncompressed, + MHD_HTTP_HEADER_ETAG, + etag)); + /* Also compute compressed version of /keys response */ + comp = TALER_MHD_body_compress (&keys_jsonz, + &keys_jsonz_size); + krd.response_compressed + = MHD_create_response_from_buffer (keys_jsonz_size, + keys_jsonz, + MHD_RESPMEM_MUST_FREE); + GNUNET_assert (NULL != krd.response_compressed); + /* If the response is actually compressed, set the + respective header. */ + GNUNET_assert ( (MHD_YES != comp) || + (MHD_YES == + MHD_add_response_header (krd.response_compressed, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) ); + setup_general_response_headers (ksh, + krd.response_compressed); + /* Information is always public, revalidate after 1 day */ + GNUNET_break (MHD_YES == + MHD_add_response_header (krd.response_compressed, + MHD_HTTP_HEADER_ETAG, + etag)); + krd.etag = GNUNET_strdup (etag); + } + krd.cherry_pick_date = last_cherry_pick_date; + GNUNET_array_append (ksh->krd_array, + ksh->krd_array_length, + krd); + return GNUNET_OK; +} + + +/** + * Element in the `struct SignatureContext` array. + */ +struct SignatureElement +{ + + /** + * Offset of the denomination in the group array, + * for sorting (2nd rank, ascending). + */ + unsigned int offset; + + /** + * Offset of the group in the denominations array, + * for sorting (2nd rank, ascending). + */ + unsigned int group_offset; + + /** + * Pointer to actual master signature to hash over. + */ + struct TALER_MasterSignatureP master_sig; +}; + +/** + * Context for collecting the array of master signatures + * needed to verify the exchange_sig online signature. + */ +struct SignatureContext +{ + /** + * Array of signatures to hash over. + */ + struct SignatureElement *elements; + + /** + * Write offset in the @e elements array. + */ + unsigned int elements_pos; + + /** + * Allocated space for @e elements. + */ + unsigned int elements_size; +}; + + +/** + * Determine order to sort two elements by before + * we hash the master signatures. Used for + * sorting with qsort(). + * + * @param a pointer to a `struct SignatureElement` + * @param b pointer to a `struct SignatureElement` + * @return 0 if equal, -1 if a < b, 1 if a > b. + */ +static int +signature_context_sort_cb (const void *a, + const void *b) +{ + const struct SignatureElement *sa = a; + const struct SignatureElement *sb = b; + + if (sa->group_offset < sb->group_offset) + return -1; + if (sa->group_offset > sb->group_offset) + return 1; + if (sa->offset < sb->offset) + return -1; + if (sa->offset > sb->offset) + return 1; + /* We should never have two disjoint elements + with same time and offset */ + GNUNET_assert (sa == sb); + return 0; +} + + +/** + * Append a @a master_sig to the @a sig_ctx using the + * given attributes for (later) sorting. + * + * @param[in,out] sig_ctx signature context to update + * @param group_offset offset for the group + * @param offset offset for the entry + * @param master_sig master signature for the entry + */ +static void +append_signature (struct SignatureContext *sig_ctx, + unsigned int group_offset, + unsigned int offset, + const struct TALER_MasterSignatureP *master_sig) +{ + struct SignatureElement *element; + unsigned int new_size; + + if (sig_ctx->elements_pos == sig_ctx->elements_size) + { + if (0 == sig_ctx->elements_size) + new_size = 1024; + else + new_size = sig_ctx->elements_size * 2; + GNUNET_array_grow (sig_ctx->elements, + sig_ctx->elements_size, + new_size); + } + element = &sig_ctx->elements[sig_ctx->elements_pos++]; + element->offset = offset; + element->group_offset = group_offset; + element->master_sig = *master_sig; +} + + +/** + *GroupData is the value we store for each group meta-data */ +struct GroupData +{ + /** + * The json blob with the group meta-data and list of denominations + */ + json_t *json; + + /** + * List of denominations for the group, + * included in @e json, do not free separately! + */ + json_t *list; + + /** + * Offset of the group in the final array. + */ + unsigned int group_off; + +}; + + +/** + * Helper function called to clean up the group data + * in the denominations_by_group below. + * + * @param cls unused + * @param key unused + * @param value a `struct GroupData` to free + * @return #GNUNET_OK + */ +static int +free_group (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct GroupData *gd = value; + + (void) cls; + (void) key; + GNUNET_free (gd); + return GNUNET_OK; +} + + +static void +compute_msig_hash (struct SignatureContext *sig_ctx, + struct GNUNET_HashCode *hc) +{ + struct GNUNET_HashContext *hash_context; + + hash_context = GNUNET_CRYPTO_hash_context_start (); + qsort (sig_ctx->elements, + sig_ctx->elements_pos, + sizeof (struct SignatureElement), + &signature_context_sort_cb); + for (unsigned int i = 0; i<sig_ctx->elements_pos; i++) + { + struct SignatureElement *element = &sig_ctx->elements[i]; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Adding %u,%u,%s\n", + element->group_offset, + element->offset, + TALER_B2S (&element->master_sig)); + GNUNET_CRYPTO_hash_context_read (hash_context, + &element->master_sig, + sizeof (element->master_sig)); + } + GNUNET_CRYPTO_hash_context_finish (hash_context, + hc); +} + + +/** + * Update the "/keys" responses in @a ksh, computing the detailed replies. + * + * This function is to recompute all (including cherry-picked) responses we + * might want to return, based on the state already in @a ksh. + * + * @param[in,out] ksh state handle to update + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +finish_keys_response (struct TEH_KeyStateHandle *ksh) +{ + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + json_t *recoup; + struct SignKeyCtx sctx = { + .min_sk_frequency = GNUNET_TIME_UNIT_FOREVER_REL + }; + json_t *grouped_denominations = NULL; + struct GNUNET_TIME_Timestamp last_cherry_pick_date; + struct GNUNET_CONTAINER_Heap *heap; + struct SignatureContext sig_ctx = { 0 }; + /* Remember if we have any denomination with age restriction */ + bool has_age_restricted_denomination = false; + struct WireStateHandle *wsh; + + wsh = get_wire_state (); + if (! wsh->ready) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 == + json_array_size (json_object_get (wsh->json_reply, + "accounts")) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No wire accounts available. Refusing to generate /keys response.\n"); + return GNUNET_NO; + } + sctx.signkeys = json_array (); + GNUNET_assert (NULL != sctx.signkeys); + recoup = json_array (); + GNUNET_assert (NULL != recoup); + grouped_denominations = json_array (); + GNUNET_assert (NULL != grouped_denominations); + + GNUNET_CONTAINER_multipeermap_iterate (ksh->signkey_map, + &add_sign_key_cb, + &sctx); + if (0 == json_array_size (sctx.signkeys)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No online signing keys available. Refusing to generate /keys response.\n"); + ret = GNUNET_NO; + goto CLEANUP; + } + heap = GNUNET_CONTAINER_heap_create (GNUNET_CONTAINER_HEAP_ORDER_MAX); + { + struct DenomKeyCtx dkc = { + .recoup = recoup, + .heap = heap, + .min_dk_frequency = GNUNET_TIME_UNIT_FOREVER_REL, + }; + + GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, + &add_denom_key_cb, + &dkc); + ksh->rekey_frequency + = GNUNET_TIME_relative_min (dkc.min_dk_frequency, + sctx.min_sk_frequency); + } + + last_cherry_pick_date = GNUNET_TIME_UNIT_ZERO_TS; + + { + struct TEH_DenominationKey *dk; + struct GNUNET_CONTAINER_MultiHashMap *denominations_by_group; + + denominations_by_group = + GNUNET_CONTAINER_multihashmap_create (1024, + GNUNET_NO /* NO, because keys are only on the stack */ + ); + /* heap = max heap, sorted by start time */ + while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap))) + { + if (GNUNET_TIME_timestamp_cmp (last_cherry_pick_date, + !=, + dk->meta.start) && + (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time)) ) + { + /* + * 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. + */ + struct GNUNET_HashCode hc; + + compute_msig_hash (&sig_ctx, + &hc); + if (GNUNET_OK != + create_krd (ksh, + &hc, + last_cherry_pick_date, + sctx.signkeys, + recoup, + grouped_denominations)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to generate key response data for %s\n", + GNUNET_TIME_timestamp2s (last_cherry_pick_date)); + /* drain heap before destroying it */ + while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap))) + /* intentionally empty */; + GNUNET_CONTAINER_heap_destroy (heap); + goto CLEANUP; + } + } + + last_cherry_pick_date = dk->meta.start; + /* + * Group the denominations by {cipher, value, fees, age_mask}. + * + * For each group we save the group meta-data and the list of + * denominations in this group as a json-blob in the multihashmap + * denominations_by_group. + */ + { + struct GroupData *group; + json_t *entry; + struct GNUNET_HashCode key; + struct TALER_DenominationGroup meta = { + .cipher = dk->denom_pub.bsign_pub_key->cipher, + .value = dk->meta.value, + .fees = dk->meta.fees, + .age_mask = dk->meta.age_mask, + }; + + /* Search the group/JSON-blob for the key */ + TALER_denomination_group_get_key (&meta, + &key); + group = GNUNET_CONTAINER_multihashmap_get ( + denominations_by_group, + &key); + if (NULL == group) + { + /* There is no group for this meta-data yet, so we create a new group */ + bool age_restricted = meta.age_mask.bits != 0; + const char *cipher; + + group = GNUNET_new (struct GroupData); + switch (meta.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + cipher = age_restricted ? "RSA+age_restricted" : "RSA"; + break; + case GNUNET_CRYPTO_BSA_CS: + cipher = age_restricted ? "CS+age_restricted" : "CS"; + break; + default: + GNUNET_assert (false); + } + /* Create a new array for the denominations in this group */ + group->list = json_array (); + GNUNET_assert (NULL != group->list); + group->json = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + cipher), + GNUNET_JSON_pack_array_steal ("denoms", + group->list), + TALER_JSON_PACK_DENOM_FEES ("fee", + &meta.fees), + TALER_JSON_pack_amount ("value", + &meta.value)); + GNUNET_assert (NULL != group->json); + if (age_restricted) + { + GNUNET_assert ( + 0 == + json_object_set_new (group->json, + "age_mask", + json_integer ( + meta.age_mask.bits))); + /* Remember that we have found at least _one_ age restricted denomination */ + has_age_restricted_denomination = true; + } + group->group_off + = json_array_size (grouped_denominations); + GNUNET_assert (0 == + json_array_append_new ( + grouped_denominations, + group->json)); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (denominations_by_group, + &key, + group, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } + + /* Now that we have found/created the right group, add the + denomination to the list */ + { + struct HelperDenomination *hd; + struct GNUNET_JSON_PackSpec key_spec; + bool private_key_lost; + + hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, + &dk->h_denom_pub.hash); + private_key_lost + = (NULL == hd) || + GNUNET_TIME_absolute_is_past ( + GNUNET_TIME_absolute_add ( + hd->start_time.abs_time, + hd->validity_duration)); + switch (meta.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + key_spec = + GNUNET_JSON_pack_rsa_public_key ( + "rsa_pub", + dk->denom_pub.bsign_pub_key->details.rsa_public_key); + break; + case GNUNET_CRYPTO_BSA_CS: + key_spec = + GNUNET_JSON_pack_data_varsize ( + "cs_pub", + &dk->denom_pub.bsign_pub_key->details.cs_public_key, + sizeof (dk->denom_pub.bsign_pub_key->details.cs_public_key)); + break; + default: + GNUNET_assert (false); + } + + entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("master_sig", + &dk->master_sig), + GNUNET_JSON_pack_allow_null ( + private_key_lost + ? GNUNET_JSON_pack_bool ("lost", + true) + : GNUNET_JSON_pack_string ("dummy", + NULL)), + GNUNET_JSON_pack_timestamp ("stamp_start", + dk->meta.start), + GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", + dk->meta.expire_withdraw), + GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", + dk->meta.expire_deposit), + GNUNET_JSON_pack_timestamp ("stamp_expire_legal", + dk->meta.expire_legal), + key_spec + ); + GNUNET_assert (NULL != entry); + } + + /* Build up the running hash of all master signatures of the + denominations */ + append_signature (&sig_ctx, + group->group_off, + (unsigned int) json_array_size (group->list), + &dk->master_sig); + /* Finally, add the denomination to the list of denominations in this + group */ + GNUNET_assert (json_is_array (group->list)); + GNUNET_assert (0 == + json_array_append_new (group->list, + entry)); + } + } /* loop over heap ends */ + + GNUNET_CONTAINER_multihashmap_iterate (denominations_by_group, + &free_group, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (denominations_by_group); + } + GNUNET_CONTAINER_heap_destroy (heap); + + if (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time)) + { + struct GNUNET_HashCode hc; + + compute_msig_hash (&sig_ctx, + &hc); + if (GNUNET_OK != + create_krd (ksh, + &hc, + last_cherry_pick_date, + sctx.signkeys, + recoup, + grouped_denominations)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to generate key response data for %s\n", + 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 + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No denomination keys available. Refusing to generate /keys response.\n"); + } + ret = GNUNET_OK; + +CLEANUP: + GNUNET_array_grow (sig_ctx.elements, + sig_ctx.elements_size, + 0); + json_decref (grouped_denominations); + if (NULL != sctx.signkeys) + json_decref (sctx.signkeys); + json_decref (recoup); + return ret; +} + + +/** + * Called with information about global fees. + * + * @param cls `struct TEH_KeyStateHandle *` we are building + * @param fees the global fees we charge + * @param purse_timeout when do purses time out + * @param history_expiration how long are account histories preserved + * @param purse_account_limit how many purses are free per account + * @param start_date from when are these fees valid (start date) + * @param end_date until when are these fees valid (end date, exclusive) + * @param master_sig master key signature affirming that this is the correct + * fee (of purpose #TALER_SIGNATURE_MASTER_GLOBAL_FEES) + */ +static void +global_fee_info_cb ( + void *cls, + const struct TALER_GlobalFeeSet *fees, + struct GNUNET_TIME_Relative purse_timeout, + struct GNUNET_TIME_Relative history_expiration, + uint32_t purse_account_limit, + struct GNUNET_TIME_Timestamp start_date, + struct GNUNET_TIME_Timestamp end_date, + const struct TALER_MasterSignatureP *master_sig) +{ + struct TEH_KeyStateHandle *ksh = cls; + struct TEH_GlobalFee *gf; + + if (GNUNET_OK != + TALER_exchange_offline_global_fee_verify ( + start_date, + end_date, + fees, + purse_timeout, + history_expiration, + purse_account_limit, + &TEH_master_public_key, + master_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database has global fee with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found global fees with %u purses\n", + purse_account_limit); + gf = GNUNET_new (struct TEH_GlobalFee); + gf->start_date = start_date; + gf->end_date = end_date; + gf->fees = *fees; + gf->purse_timeout = purse_timeout; + gf->history_expiration = history_expiration; + gf->purse_account_limit = purse_account_limit; + gf->master_sig = *master_sig; + GNUNET_CONTAINER_DLL_insert (ksh->gf_head, + ksh->gf_tail, + gf); + GNUNET_assert ( + 0 == + json_array_append_new ( + ksh->global_fees, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("start_date", + start_date), + GNUNET_JSON_pack_timestamp ("end_date", + end_date), + TALER_JSON_PACK_GLOBAL_FEES (fees), + GNUNET_JSON_pack_time_rel ("history_expiration", + history_expiration), + GNUNET_JSON_pack_time_rel ("purse_timeout", + purse_timeout), + GNUNET_JSON_pack_uint64 ("purse_account_limit", + purse_account_limit), + GNUNET_JSON_pack_data_auto ("master_sig", + master_sig)))); +} + + +/** + * Create a key state. + * + * @param management_only if we should NOT run 'finish_keys_response()' + * because we only need the state for the /management/keys API + * @return NULL on error (i.e. failed to access database) + */ +static struct TEH_KeyStateHandle * +build_key_state (bool management_only) +{ + struct TEH_KeyStateHandle *ksh; + enum GNUNET_DB_QueryStatus qs; + + ksh = GNUNET_new (struct TEH_KeyStateHandle); + ksh->signature_expires = GNUNET_TIME_UNIT_FOREVER_TS; + ksh->reload_time = GNUNET_TIME_timestamp_get (); + /* We must use the key_generation from when we STARTED the process! */ + ksh->key_generation = key_generation; + ksh->denomserial_map = GNUNET_CONTAINER_multihashmap32_create (1024); + ksh->denomkey_map = GNUNET_CONTAINER_multihashmap_create (1024, + true); + ksh->signkey_map = GNUNET_CONTAINER_multipeermap_create (32, + false /* MUST be false! */ + ); + ksh->auditors = json_array (); + GNUNET_assert (NULL != ksh->auditors); + /* NOTE: fetches master-signed signkeys, but ALSO those that were revoked! */ + GNUNET_break (GNUNET_OK == + TEH_plugin->preflight (TEH_plugin->cls)); + if (NULL != ksh->global_fees) + json_decref (ksh->global_fees); + ksh->global_fees = json_array (); + qs = TEH_plugin->get_global_fees (TEH_plugin->cls, + &global_fee_info_cb, + ksh); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Loading global fees from DB: %d\n", + qs); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + destroy_key_state (ksh); + return NULL; + } + qs = TEH_plugin->iterate_denominations (TEH_plugin->cls, + &denomination_info_cb, + ksh); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + destroy_key_state (ksh); + return NULL; + } + /* NOTE: ONLY fetches non-revoked AND master-signed signkeys! */ + qs = TEH_plugin->iterate_active_signkeys (TEH_plugin->cls, + &signkey_info_cb, + ksh); + if (qs < 0) + { + GNUNET_break (0); + destroy_key_state (ksh); + return NULL; + } + qs = TEH_plugin->iterate_auditor_denominations (TEH_plugin->cls, + &auditor_denom_cb, + ksh); + if (qs < 0) + { + GNUNET_break (0); + destroy_key_state (ksh); + return NULL; + } + qs = TEH_plugin->iterate_active_auditors (TEH_plugin->cls, + &auditor_info_cb, + ksh); + if (qs < 0) + { + GNUNET_break (0); + destroy_key_state (ksh); + return NULL; + } + + if (management_only) + { + ksh->management_only = true; + return ksh; + } + + if (GNUNET_OK != + finish_keys_response (ksh)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Could not finish /keys response (required data not configured yet)\n"); + destroy_key_state (ksh); + return NULL; + } + TEH_resume_keys_requests (false); + return ksh; +} + + +void +TEH_keys_update_states () +{ + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), + }; + + TEH_plugin->event_notify (TEH_plugin->cls, + &es, + NULL, + 0); + key_generation++; + TEH_resume_keys_requests (false); +} + + +static struct TEH_KeyStateHandle * +keys_get_state (bool management_only) +{ + struct TEH_KeyStateHandle *old_ksh; + struct TEH_KeyStateHandle *ksh; + + old_ksh = key_state; + if (NULL == old_ksh) + { + ksh = build_key_state (management_only); + if (NULL == ksh) + return NULL; + key_state = ksh; + return ksh; + } + if ( (old_ksh->key_generation < key_generation) || + (GNUNET_TIME_absolute_is_past (old_ksh->signature_expires.abs_time)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Rebuilding /keys, generation upgrade from %llu to %llu\n", + (unsigned long long) old_ksh->key_generation, + (unsigned long long) key_generation); + ksh = build_key_state (management_only); + key_state = ksh; + destroy_key_state (old_ksh); + return ksh; + } + sync_key_helpers (&helpers); + return old_ksh; +} + + +struct TEH_KeyStateHandle * +TEH_keys_get_state_for_management_only (void) +{ + return keys_get_state (true); +} + + +struct TEH_KeyStateHandle * +TEH_keys_get_state (void) +{ + struct TEH_KeyStateHandle *ksh; + + ksh = keys_get_state (false); + if (NULL == ksh) + return NULL; + + if (ksh->management_only) + { + if (GNUNET_OK != + finish_keys_response (ksh)) + return NULL; + } + + return ksh; +} + + +const struct TEH_GlobalFee * +TEH_keys_global_fee_by_time ( + struct TEH_KeyStateHandle *ksh, + struct GNUNET_TIME_Timestamp ts) +{ + for (const struct TEH_GlobalFee *gf = ksh->gf_head; + NULL != gf; + gf = gf->next) + { + if (GNUNET_TIME_timestamp_cmp (ts, + >=, + gf->start_date) && + GNUNET_TIME_timestamp_cmp (ts, + <, + gf->end_date)) + return gf; + } + return NULL; +} + + +struct TEH_DenominationKey * +TEH_keys_denomination_by_hash ( + const struct TALER_DenominationHashP *h_denom_pub, + struct MHD_Connection *conn, + MHD_RESULT *mret) +{ + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + *mret = TALER_MHD_reply_with_error (conn, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return NULL; + } + + return TEH_keys_denomination_by_hash_from_state (ksh, + h_denom_pub, + conn, + mret); +} + + +struct TEH_DenominationKey * +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) +{ + struct TEH_DenominationKey *dk; + + dk = GNUNET_CONTAINER_multihashmap_get (ksh->denomkey_map, + &h_denom_pub->hash); + if (NULL == dk) + { + if (NULL == conn) + return NULL; + *mret = TEH_RESPONSE_reply_unknown_denom_pub_hash (conn, + h_denom_pub); + return NULL; + } + return dk; +} + + +struct TEH_DenominationKey * +TEH_keys_denomination_by_serial_from_state ( + const struct TEH_KeyStateHandle *ksh, + uint64_t denom_serial) +{ + struct TEH_DenominationKey *dk; + uint32_t serial32 = (uint32_t) denom_serial; + + GNUNET_assert (denom_serial == (uint64_t) serial32); + dk = GNUNET_CONTAINER_multihashmap32_get (ksh->denomserial_map, + serial32); + return dk; +} + + +enum TALER_ErrorCode +TEH_keys_denomination_batch_sign ( + unsigned int csds_length, + const struct TEH_CoinSignData csds[static csds_length], + bool for_melt, + struct TALER_BlindedDenominationSignature bss[static csds_length]) +{ + struct TEH_KeyStateHandle *ksh; + struct HelperDenomination *hd; + struct TALER_CRYPTO_RsaSignRequest rsrs[csds_length]; + struct TALER_CRYPTO_CsSignRequest csrs[csds_length]; + struct TALER_BlindedDenominationSignature rs[csds_length]; + struct TALER_BlindedDenominationSignature cs[csds_length]; + unsigned int rsrs_pos = 0; + unsigned int csrs_pos = 0; + enum TALER_ErrorCode ec; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; + for (unsigned int i = 0; i<csds_length; i++) + { + const struct TALER_DenominationHashP *h_denom_pub = csds[i].h_denom_pub; + const struct TALER_BlindedPlanchet *bp = csds[i].bp; + + hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, + &h_denom_pub->hash); + if (NULL == hd) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + if (bp->blinded_message->cipher != + hd->denom_pub.bsign_pub_key->cipher) + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + switch (hd->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + rsrs[rsrs_pos].h_rsa = &hd->h_details.h_rsa; + rsrs[rsrs_pos].msg + = bp->blinded_message->details.rsa_blinded_message.blinded_msg; + rsrs[rsrs_pos].msg_size + = bp->blinded_message->details.rsa_blinded_message.blinded_msg_size; + rsrs_pos++; + break; + case GNUNET_CRYPTO_BSA_CS: + csrs[csrs_pos].h_cs = &hd->h_details.h_cs; + csrs[csrs_pos].blinded_planchet + = &bp->blinded_message->details.cs_blinded_message; + csrs_pos++; + break; + default: + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + } + + if (0 != rsrs_pos) + { + memset (rs, + 0, + sizeof (rs)); + } + if (0 != csrs_pos) + { + memset (cs, + 0, + sizeof (cs)); + } + ec = TALER_EC_NONE; + if (0 != csrs_pos) + { + ec = TALER_CRYPTO_helper_cs_batch_sign ( + helpers.csdh, + csrs_pos, + csrs, + for_melt, + (0 == rsrs_pos) ? bss : cs); + if (TALER_EC_NONE != ec) + { + for (unsigned int i = 0; i<csrs_pos; i++) + TALER_blinded_denom_sig_free (&cs[i]); + return ec; + } + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_CS] += csrs_pos; + } + if (0 != rsrs_pos) + { + ec = TALER_CRYPTO_helper_rsa_batch_sign ( + helpers.rsadh, + rsrs_pos, + rsrs, + (0 == csrs_pos) ? bss : rs); + if (TALER_EC_NONE != ec) + { + for (unsigned int i = 0; i<csrs_pos; i++) + TALER_blinded_denom_sig_free (&cs[i]); + for (unsigned int i = 0; i<rsrs_pos; i++) + TALER_blinded_denom_sig_free (&rs[i]); + return ec; + } + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA] += rsrs_pos; + } + + if ( (0 != csrs_pos) && + (0 != rsrs_pos) ) + { + rsrs_pos = 0; + csrs_pos = 0; + for (unsigned int i = 0; i<csds_length; i++) + { + const struct TALER_BlindedPlanchet *bp = csds[i].bp; + + switch (bp->blinded_message->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + bss[i] = rs[rsrs_pos++]; + break; + case GNUNET_CRYPTO_BSA_CS: + bss[i] = cs[csrs_pos++]; + break; + default: + GNUNET_assert (0); + } + } + } + return TALER_EC_NONE; +} + + +enum TALER_ErrorCode +TEH_keys_denomination_cs_r_pub ( + const struct TEH_CsDeriveData *cdd, + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP *r_pub) +{ + const struct TALER_DenominationHashP *h_denom_pub = cdd->h_denom_pub; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdd->nonce; + struct TEH_KeyStateHandle *ksh; + struct HelperDenomination *hd; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; + } + hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, + &h_denom_pub->hash); + if (NULL == hd) + { + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + } + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) + { + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + + { + struct TALER_CRYPTO_CsDeriveRequest cdr = { + .h_cs = &hd->h_details.h_cs, + .nonce = nonce + }; + return TALER_CRYPTO_helper_cs_r_derive (helpers.csdh, + &cdr, + for_melt, + r_pub); + } +} + + +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub_simple ( + unsigned int cdds_length, + const struct TEH_CsDeriveData cdds[static cdds_length], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]) +{ + struct TEH_KeyStateHandle *ksh; + struct HelperDenomination *hd; + struct TALER_CRYPTO_CsDeriveRequest cdrs[cdds_length]; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; + } + for (unsigned int i = 0; i<cdds_length; i++) + { + const struct TALER_DenominationHashP *h_denom_pub = cdds[i].h_denom_pub; + const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdds[i].nonce; + + hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, + &h_denom_pub->hash); + if (NULL == hd) + { + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + } + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) + { + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + cdrs[i].h_cs = &hd->h_details.h_cs; + cdrs[i].nonce = nonce; + } + + return TALER_CRYPTO_helper_cs_r_batch_derive (helpers.csdh, + cdds_length, + cdrs, + for_melt, + r_pubs); +} + + +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub ( + const struct TEH_KeyStateHandle *ksh, + size_t num, + const struct TALER_DenominationHashP h_denom_pubs[static num], + const struct GNUNET_CRYPTO_CsSessionNonce nonces[static num], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static num], + size_t *err_idx) +{ + struct TALER_CRYPTO_CsDeriveRequest cdrs[num]; + + for (unsigned int i = 0; i<num; i++) + { + const struct TEH_DenominationKey *dk; + const struct HelperDenomination *hd; + + *err_idx = i; + + /* FIXME: right now we need both, + * TEH_DenominationKey and HelperDenomination, + * because only TEH_DenominationKey has .recoup_possible + */ + hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, + &h_denom_pubs[i].hash); + dk = TEH_keys_denomination_by_hash_from_state (ksh, + &h_denom_pubs[i], + NULL, + NULL); + if (NULL == hd) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + + GNUNET_assert (NULL != dk); + + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) + return TALER_EC_EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION + ; + + if (GNUNET_TIME_absolute_is_future (hd->start_time.abs_time)) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE; + + if (dk->recoup_possible) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED; + + if (GNUNET_TIME_relative_is_zero (hd->validity_duration)) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED; + + cdrs[i].h_cs = &hd->h_details.h_cs; + cdrs[i].nonce = &nonces[i]; + } + + return TALER_CRYPTO_helper_cs_r_batch_derive (helpers.csdh, + num, + cdrs, + for_melt, + r_pubs); +} + + +void +TEH_keys_denomination_revoke (const struct TALER_DenominationHashP *h_denom_pub) +{ + struct TEH_KeyStateHandle *ksh; + struct HelperDenomination *hd; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + return; + } + hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, + &h_denom_pub->hash); + if (NULL == hd) + { + GNUNET_break (0); + return; + } + switch (hd->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + break; + case GNUNET_CRYPTO_BSA_RSA: + TALER_CRYPTO_helper_rsa_revoke (helpers.rsadh, + &hd->h_details.h_rsa); + TEH_keys_update_states (); + return; + case GNUNET_CRYPTO_BSA_CS: + TALER_CRYPTO_helper_cs_revoke (helpers.csdh, + &hd->h_details.h_cs); + TEH_keys_update_states (); + return; + } + GNUNET_break (0); + return; +} + + +enum TALER_ErrorCode +TEH_keys_exchange_sign_ ( + const struct GNUNET_CRYPTO_SignaturePurpose *purpose, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig) +{ + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + /* This *can* happen if the exchange's crypto helper is not running + or had some bad error. */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Cannot sign request, no valid signing keys available.\n"); + return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; + } + return TEH_keys_exchange_sign2_ (ksh, + purpose, + pub, + sig); +} + + +enum TALER_ErrorCode +TEH_keys_exchange_sign2_ ( + void *cls, + const struct GNUNET_CRYPTO_SignaturePurpose *purpose, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig) +{ + struct TEH_KeyStateHandle *ksh = cls; + enum TALER_ErrorCode ec; + + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_EDDSA]++; + ec = TALER_CRYPTO_helper_esign_sign_ (helpers.esh, + purpose, + pub, + sig); + if (TALER_EC_NONE != ec) + return ec; + { + /* Here we check here that 'pub' is set to an exchange public key that is + actually signed by the master key! Otherwise, we happily continue to + use key material even if the offline signatures have not been made + yet! */ + struct GNUNET_PeerIdentity pid; + struct SigningKey *sk; + + pid.public_key = pub->eddsa_pub; + sk = GNUNET_CONTAINER_multipeermap_get (ksh->signkey_map, + &pid); + if (NULL == sk) + { + /* just to be safe, zero out the (valid) signature, as the key + should not or no longer be used */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot sign, offline key signatures are missing!\n"); + memset (sig, + 0, + sizeof (*sig)); + return TALER_EC_EXCHANGE_SIGNKEY_HELPER_OFFLINE_MISSING; + } + } + return ec; +} + + +void +TEH_keys_exchange_revoke (const struct TALER_ExchangePublicKeyP *exchange_pub) +{ + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + return; + } + TALER_CRYPTO_helper_esign_revoke (helpers.esh, + exchange_pub); + TEH_keys_update_states (); +} + + +/** + * Comparator used for a binary search by cherry_pick_date for @a key in the + * `struct KeysResponseData` array. See libc's qsort() and bsearch() functions. + * + * @param key pointer to a `struct GNUNET_TIME_Timestamp` + * @param value pointer to a `struct KeysResponseData` array entry + * @return 0 if time matches, -1 if key is smaller, 1 if key is larger + */ +static int +krd_search_comparator (const void *key, + const void *value) +{ + const struct GNUNET_TIME_Timestamp *kd = key; + const struct KeysResponseData *krd = value; + + if (GNUNET_TIME_timestamp_cmp (*kd, + >, + krd->cherry_pick_date)) + return -1; + if (GNUNET_TIME_timestamp_cmp (*kd, + <, + krd->cherry_pick_date)) + return 1; + return 0; +} + + +MHD_RESULT +TEH_keys_get_handler (struct TEH_RequestContext *rc, + const char *const args[]) +{ + struct GNUNET_TIME_Timestamp last_issue_date; + const char *etag; + + etag = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + (void) args; + { + const char *have_cherrypick; + + have_cherrypick = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "last_issue_date"); + if (NULL != have_cherrypick) + { + unsigned long long cherrypickn; + + if (1 != + sscanf (have_cherrypick, + "%llu", + &cherrypickn)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + have_cherrypick); + } + /* The following multiplication may overflow; but this should not really + be a problem, as giving back 'older' data than what the client asks for + (given that the client asks for data in the distant future) is not + problematic */ + last_issue_date = GNUNET_TIME_timestamp_from_s (cherrypickn); + } + else + { + last_issue_date = GNUNET_TIME_UNIT_ZERO_TS; + } + } + + { + struct TEH_KeyStateHandle *ksh; + const struct KeysResponseData *krd; + + ksh = TEH_keys_get_state (); + if ( (NULL == ksh) || + (0 == ksh->krd_array_length) ) + { + if ( ( (SKR_LIMIT <= skr_size) && + (GNUNET_TIME_relative_cmp ( + GNUNET_TIME_absolute_get_duration (rc->start_time), + >, + GNUNET_TIME_UNIT_SECONDS)) ) || + TEH_suicide) + { + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + TEH_suicide + ? "server terminating" + : "too many connections suspended waiting on /keys"); + } + return suspend_request (rc->connection); + } + krd = bsearch (&last_issue_date, + ksh->krd_array, + ksh->krd_array_length, + sizeof (struct KeysResponseData), + &krd_search_comparator); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Filtering /keys by cherry pick date %s found entry %u/%u\n", + GNUNET_TIME_timestamp2s (last_issue_date), + (unsigned int) (krd - ksh->krd_array), + ksh->krd_array_length); + if ( (NULL == krd) && + (ksh->krd_array_length > 0) ) + { + if (! GNUNET_TIME_absolute_is_zero (last_issue_date.abs_time)) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client provided invalid cherry picking timestamp %s, returning full response\n", + GNUNET_TIME_timestamp2s (last_issue_date)); + krd = &ksh->krd_array[ksh->krd_array_length - 1]; + } + if (NULL == krd) + { + /* Likely keys not ready *yet*. + Wait until they are. */ + return suspend_request (rc->connection); + } + if ( (NULL != etag) && + (0 == strcmp (etag, + krd->etag)) ) + return TEH_RESPONSE_reply_not_modified (rc->connection, + krd->etag, + &setup_general_response_headers, + ksh); + + return MHD_queue_response ( + rc->connection, + MHD_HTTP_OK, + (TALER_MHD_CT_DEFLATE == + TALER_MHD_can_compress (rc->connection, + TALER_MHD_CT_DEFLATE)) + ? krd->response_compressed + : krd->response_uncompressed); + } +} + + +/** + * Load extension data, like fees, expiration times (!) and age restriction + * flags for the denomination type configured in section @a section_name. + * Before calling this function, the `start` and `validity_duration` times must + * already be initialized in @a meta. + * + * @param section_name section in the configuration to use + * @param[in,out] meta denomination type data to complete + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +load_extension_data (const char *section_name, + struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta) +{ + struct GNUNET_TIME_Relative deposit_duration; + struct GNUNET_TIME_Relative legal_duration; + + GNUNET_assert (! GNUNET_TIME_absolute_is_zero (meta->start.abs_time)); /* caller bug */ + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (TEH_cfg, + section_name, + "DURATION_SPEND", + &deposit_duration)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section_name, + "DURATION_SPEND"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (TEH_cfg, + section_name, + "DURATION_LEGAL", + &legal_duration)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section_name, + "DURATION_LEGAL"); + return GNUNET_SYSERR; + } + meta->expire_deposit + = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (meta->expire_withdraw.abs_time, + deposit_duration)); + meta->expire_legal = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (meta->expire_deposit.abs_time, + legal_duration)); + if (GNUNET_OK != + TALER_config_get_amount (TEH_cfg, + section_name, + "VALUE", + &meta->value)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "Need amount for option `%s' in section `%s'\n", + "VALUE", + section_name); + return GNUNET_SYSERR; + } + if (0 != strcasecmp (TEH_currency, + meta->value.currency)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Need denomination value in section `%s' to use currency `%s'\n", + section_name, + TEH_currency); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_config_get_denom_fees (TEH_cfg, + TEH_currency, + section_name, + &meta->fees)) + return GNUNET_SYSERR; + meta->age_mask = load_age_mask (section_name); + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TEH_keys_load_fees (struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *h_denom_pub, + struct TALER_DenominationPublicKey *denom_pub, + struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta) +{ + struct HelperDenomination *hd; + enum GNUNET_GenericReturnValue ok; + + hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, + &h_denom_pub->hash); + if (NULL == hd) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Denomination %s not known\n", + GNUNET_h2s (&h_denom_pub->hash)); + return GNUNET_NO; + } + meta->start = hd->start_time; + meta->expire_withdraw = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (meta->start.abs_time, + hd->validity_duration)); + ok = load_extension_data (hd->section_name, + meta); + if (GNUNET_OK == ok) + { + GNUNET_assert (GNUNET_CRYPTO_BSA_INVALID != + hd->denom_pub.bsign_pub_key->cipher); + TALER_denom_pub_copy (denom_pub, + &hd->denom_pub); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No fees for `%s', voiding key\n", + hd->section_name); + memset (denom_pub, + 0, + sizeof (*denom_pub)); + } + return ok; +} + + +enum GNUNET_GenericReturnValue +TEH_keys_get_timing (const struct TALER_ExchangePublicKeyP *exchange_pub, + struct TALER_EXCHANGEDB_SignkeyMetaData *meta) +{ + struct TEH_KeyStateHandle *ksh; + struct HelperSignkey *hsk; + struct GNUNET_PeerIdentity pid; + + ksh = TEH_keys_get_state_for_management_only (); + if (NULL == ksh) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + pid.public_key = exchange_pub->eddsa_pub; + hsk = GNUNET_CONTAINER_multipeermap_get (helpers.esign_keys, + &pid); + if (NULL == hsk) + { + GNUNET_break (0); + return GNUNET_NO; + } + meta->start = hsk->start_time; + + meta->expire_sign = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (meta->start.abs_time, + hsk->validity_duration)); + meta->expire_legal = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (meta->expire_sign.abs_time, + signkey_legal_duration)); + return GNUNET_OK; +} + + +/** + * Closure for #add_future_denomkey_cb and #add_future_signkey_cb. + */ +struct FutureBuilderContext +{ + /** + * Our key state. + */ + struct TEH_KeyStateHandle *ksh; + + /** + * Array of denomination keys. + */ + json_t *denoms; + + /** + * Array of signing keys. + */ + json_t *signkeys; + +}; + + +/** + * Function called on all of our current and future denomination keys + * known to the helper process. Filters out those that are current + * and adds the remaining denomination keys (with their configuration + * data) to the JSON array. + * + * @param cls the `struct FutureBuilderContext *` + * @param h_denom_pub hash of the denomination public key + * @param value a `struct HelperDenomination` + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +add_future_denomkey_cb (void *cls, + const struct GNUNET_HashCode *h_denom_pub, + void *value) +{ + struct FutureBuilderContext *fbc = cls; + struct HelperDenomination *hd = value; + struct TEH_DenominationKey *dk; + struct TALER_EXCHANGEDB_DenominationKeyMetaData meta = {0}; + + dk = GNUNET_CONTAINER_multihashmap_get (fbc->ksh->denomkey_map, + h_denom_pub); + if (NULL != dk) + return GNUNET_OK; /* skip: this key is already active! */ + if (GNUNET_TIME_relative_is_zero (hd->validity_duration)) + return GNUNET_OK; /* this key already expired! */ + meta.start = hd->start_time; + meta.expire_withdraw = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (meta.start.abs_time, + hd->validity_duration)); + if (GNUNET_OK != + load_extension_data (hd->section_name, + &meta)) + { + /* Woops, couldn't determine fee structure!? */ + return GNUNET_OK; + } + GNUNET_assert ( + 0 == + json_array_append_new ( + fbc->denoms, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("value", + &meta.value), + GNUNET_JSON_pack_timestamp ("stamp_start", + meta.start), + GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", + meta.expire_withdraw), + GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", + meta.expire_deposit), + GNUNET_JSON_pack_timestamp ("stamp_expire_legal", + meta.expire_legal), + TALER_JSON_pack_denom_pub ("denom_pub", + &hd->denom_pub), + TALER_JSON_PACK_DENOM_FEES ("fee", + &meta.fees), + GNUNET_JSON_pack_data_auto ("denom_secmod_sig", + &hd->sm_sig), + GNUNET_JSON_pack_string ("section_name", + hd->section_name)))); + return GNUNET_OK; +} + + +/** + * Function called on all of our current and future exchange signing keys + * known to the helper process. Filters out those that are current + * and adds the remaining signing keys (with their configuration + * data) to the JSON array. + * + * @param cls the `struct FutureBuilderContext *` + * @param pid actually the exchange public key (type disguised) + * @param value a `struct HelperDenomination` + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +add_future_signkey_cb (void *cls, + const struct GNUNET_PeerIdentity *pid, + void *value) +{ + struct FutureBuilderContext *fbc = cls; + struct HelperSignkey *hsk = value; + struct SigningKey *sk; + struct GNUNET_TIME_Timestamp stamp_expire; + struct GNUNET_TIME_Timestamp legal_end; + + sk = GNUNET_CONTAINER_multipeermap_get (fbc->ksh->signkey_map, + pid); + if (NULL != sk) + return GNUNET_OK; /* skip: this key is already active */ + if (GNUNET_TIME_relative_is_zero (hsk->validity_duration)) + return GNUNET_OK; /* this key already expired! */ + stamp_expire = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (hsk->start_time.abs_time, + hsk->validity_duration)); + legal_end = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (stamp_expire.abs_time, + signkey_legal_duration)); + GNUNET_assert (0 == + json_array_append_new ( + fbc->signkeys, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("key", + &hsk->exchange_pub), + GNUNET_JSON_pack_timestamp ("stamp_start", + hsk->start_time), + GNUNET_JSON_pack_timestamp ("stamp_expire", + stamp_expire), + GNUNET_JSON_pack_timestamp ("stamp_end", + legal_end), + GNUNET_JSON_pack_data_auto ("signkey_secmod_sig", + &hsk->sm_sig)))); + return GNUNET_OK; +} + + +MHD_RESULT +TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + struct TEH_KeyStateHandle *ksh; + json_t *reply; + + (void) rh; + ksh = TEH_keys_get_state_for_management_only (); + if (NULL == ksh) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + "no key state"); + } + sync_key_helpers (&helpers); + if (NULL == ksh->management_keys_reply) + { + struct FutureBuilderContext fbc = { + .ksh = ksh, + .denoms = json_array (), + .signkeys = json_array () + }; + + GNUNET_assert (NULL != fbc.denoms); + GNUNET_assert (NULL != fbc.signkeys); + if ( (GNUNET_is_zero (&denom_rsa_sm_pub)) && + (GNUNET_is_zero (&denom_cs_sm_pub)) ) + { + /* Either IPC failed, or neither helper had any denominations configured. */ + json_decref (fbc.denoms); + json_decref (fbc.signkeys); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_GATEWAY, + TALER_EC_EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE, + NULL); + } + if (GNUNET_is_zero (&esign_sm_pub)) + { + json_decref (fbc.denoms); + json_decref (fbc.signkeys); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_GATEWAY, + TALER_EC_EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE, + NULL); + } + GNUNET_CONTAINER_multihashmap_iterate (helpers.denom_keys, + &add_future_denomkey_cb, + &fbc); + GNUNET_CONTAINER_multipeermap_iterate (helpers.esign_keys, + &add_future_signkey_cb, + &fbc); + reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("future_denoms", + fbc.denoms), + GNUNET_JSON_pack_array_steal ("future_signkeys", + fbc.signkeys), + GNUNET_JSON_pack_data_auto ("master_pub", + &TEH_master_public_key), + GNUNET_JSON_pack_data_auto ("denom_secmod_public_key", + &denom_rsa_sm_pub), + GNUNET_JSON_pack_data_auto ("denom_secmod_cs_public_key", + &denom_cs_sm_pub), + GNUNET_JSON_pack_data_auto ("signkey_secmod_public_key", + &esign_sm_pub)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning GET /management/keys response:\n"); + if (NULL == reply) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + NULL); + } + GNUNET_assert (NULL == ksh->management_keys_reply); + ksh->management_keys_reply = reply; + } + else + { + reply = ksh->management_keys_reply; + } + return TALER_MHD_reply_json (connection, + reply, + MHD_HTTP_OK); +} + + +/* end of taler-exchange-httpd_keys.c */ diff --git a/src/exchange/taler-exchange-httpd_get-keys.h b/src/exchange/taler-exchange-httpd_get-keys.h @@ -0,0 +1,614 @@ +/* + This file is part of TALER + Copyright (C) 2020-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-keys.h + * @brief management of our various keys + * @defgroup crypto Cryptographic routines + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_responses.h" + + +#ifndef TALER_EXCHANGE_HTTPD_GET_KEYS_H +#define TALER_EXCHANGE_HTTPD_GET_KEYS_H + +/** + * Signatures of an auditor over a denomination key of this exchange. + */ +struct TEH_AuditorSignature; + + +/** + * @brief All information about a denomination key (which is used to + * sign coins into existence). + */ +struct TEH_DenominationKey +{ + + /** + * Decoded denomination public key (the hash of it is in + * @e issue, but we sometimes need the full public key as well). + */ + struct TALER_DenominationPublicKey denom_pub; + + /** + * Hash code of the denomination public key. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Meta data about the type of the denomination, such as fees and validity + * periods. + */ + struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; + + /** + * The long-term offline master key's signature for this denomination. + * Signs over @e h_denom_pub and @e meta. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * We store the auditor signatures for this denomination in a DLL. + */ + struct TEH_AuditorSignature *as_head; + + /** + * We store the auditor signatures for this denomination in a DLL. + */ + struct TEH_AuditorSignature *as_tail; + + /** + * Set to 'true' if this denomination has been revoked and recoup is + * thus supported right now. + */ + bool recoup_possible; + +}; + + +/** + * Set of global fees (and options) for a time range. + */ +struct TEH_GlobalFee +{ + /** + * Kept in a DLL. + */ + struct TEH_GlobalFee *next; + + /** + * Kept in a DLL. + */ + struct TEH_GlobalFee *prev; + + /** + * Beginning of the validity period (inclusive). + */ + struct GNUNET_TIME_Timestamp start_date; + + /** + * End of the validity period (exclusive). + */ + struct GNUNET_TIME_Timestamp end_date; + + /** + * How long do unmerged purses stay around at most? + */ + struct GNUNET_TIME_Relative purse_timeout; + + /** + * What is the longest history we return? + */ + struct GNUNET_TIME_Relative history_expiration; + + /** + * Signature affirming these details. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Fee structure for operations that do not depend + * on a denomination or wire method. + */ + struct TALER_GlobalFeeSet fees; + + /** + * Number of free purses per account. + */ + uint32_t purse_account_limit; +}; + + +/** + * Snapshot of the (coin and signing) keys (including private keys) of + * the exchange. There can be multiple instances of this struct, as it is + * reference counted and only destroyed once the last user is done + * with it. The current instance is acquired using + * #TEH_KS_acquire(). Using this function increases the + * reference count. The contents of this structure (except for the + * reference counter) should be considered READ-ONLY until it is + * ultimately destroyed (as there can be many concurrent users). + */ +struct TEH_KeyStateHandle; + + +/** + * Run internal invariant checks. For debugging. + */ +void +TEH_check_invariants (void); + + +/** + * Look up wire fee structure by @a ts. + * + * @param ts timestamp to lookup wire fees at + * @param method wire method to lookup fees for + * @return the wire fee details, or + * NULL if none are configured for @a ts and @a method + */ +const struct TALER_WireFeeSet * +TEH_wire_fees_by_time ( + struct GNUNET_TIME_Timestamp ts, + const char *method); + + +/** + * Something changed in the database. Rebuild the wire replies. This function + * should be called if the exchange learns about a new signature from our + * master key. + * + * (We do not do so immediately, but merely signal to all threads that they + * need to rebuild their wire state upon the next call to + * #TEH_keys_get_state()). + */ +void +TEH_wire_update_state (void); + + +/** + * Return the current key state for this thread. Possibly re-builds the key + * state if we have reason to believe that something changed. + * + * The result is ONLY valid until the next call to + * #TEH_keys_denomination_by_hash() or #TEH_keys_get_state() + * or #TEH_keys_exchange_sign(). + * + * @return NULL on error + */ +struct TEH_KeyStateHandle * +TEH_keys_get_state (void); + +/** + * Obtain the key state if we should NOT run finish_keys_response() because we + * only need the state for the /management/keys API + */ +struct TEH_KeyStateHandle * +TEH_keys_get_state_for_management_only (void); + +/** + * Something changed in the database. Rebuild all key states. This function + * should be called if the exchange learns about a new signature from an + * auditor or our master key. + * + * (We do not do so immediately, but merely signal to all threads that they + * need to rebuild their key state upon the next call to + * #TEH_keys_get_state()). + */ +void +TEH_keys_update_states (void); + + +/** + * Look up global fee structure by @a ts. + * + * @param ksh key state state to look in + * @param ts timestamp to lookup global fees at + * @return the global fee details, or + * NULL if none are configured for @a ts + */ +const struct TEH_GlobalFee * +TEH_keys_global_fee_by_time ( + struct TEH_KeyStateHandle *ksh, + struct GNUNET_TIME_Timestamp ts); + + +/** + * Look up the issue for a denom public key. Note that the result + * must only be used in this thread and only until another key or + * key state is resolved. + * + * @param h_denom_pub hash of denomination public key + * @param[in,out] conn used to return status message if NULL is returned + * @param[out] mret set to the MHD status if NULL is returned + * @return the denomination key issue, + * or NULL if @a h_denom_pub could not be found + */ +struct TEH_DenominationKey * +TEH_keys_denomination_by_hash ( + const struct TALER_DenominationHashP *h_denom_pub, + struct MHD_Connection *conn, + MHD_RESULT *mret); + + +/** + * Look up the issue for a denom public key using a given @a ksh. This allows + * requesting multiple denominations with the same @a ksh which thus will + * remain valid until the next call to #TEH_keys_denomination_by_hash() or + * #TEH_keys_get_state() or #TEH_keys_exchange_sign(). + * + * @param ksh key state state to look in + * @param h_denom_pub hash of denomination public key + * @param[in,out] conn connection used to return status message if NULL is returned + * @param[out] mret set to the MHD status if NULL is returned + * @return the denomination key issue, + * or NULL if @a h_denom_pub could not be found + */ +struct TEH_DenominationKey * +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); + + +/** + * Look up the issue for a denom public key using a given @a ksh. This allows + * requesting multiple denominations with the same @a ksh which thus will + * remain valid until the next call to #TEH_keys_denomination_by_hash() or + * #TEH_keys_get_state() or #TEH_keys_exchange_sign(). + * + * @param ksh key state state to look in + * @param denom_serial serial ID of the denomination in the table + * @return the denomination key issue, + * or NULL if @a denom_serial could not be found + */ +struct TEH_DenominationKey * +TEH_keys_denomination_by_serial_from_state ( + const struct TEH_KeyStateHandle *ksh, + uint64_t denom_serial); + + +/** + * Information needed to create a blind signature. + */ +struct TEH_CoinSignData +{ + /** + * Hash of key to sign with. + */ + const struct TALER_DenominationHashP *h_denom_pub; + + /** + * Blinded planchet to sign over. + */ + const struct TALER_BlindedPlanchet *bp; +}; + + +/** + * Request to sign @a csds. + * + * @param csds array with data to blindly sign (and keys to sign with) + * @param csds_length length of @a csds array + * @param for_melt true if this is for a melt operation + * @param[out] bss array set to the blind signature on success; must be of length @a csds_length + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_denomination_batch_sign ( + unsigned int csds_length, + const struct TEH_CoinSignData csds[static csds_length], + bool for_melt, + struct TALER_BlindedDenominationSignature bss[static csds_length]); + + +/** + * Information needed to derive the CS r_pub. + */ +struct TEH_CsDeriveData +{ + /** + * Hash of key to sign with. + */ + const struct TALER_DenominationHashP *h_denom_pub; + + /** + * Nonce to use. + */ + const struct GNUNET_CRYPTO_CsSessionNonce *nonce; +}; + + +/** + * Request to derive CS @a r_pub using the denomination and nonce from @a cdd. + * + * @param cdd data to compute @a r_pub from + * @param for_melt true if this is for a melt operation + * @param[out] r_pub where to write the result + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_denomination_cs_r_pub ( + const struct TEH_CsDeriveData *cdd, + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP *r_pub); + + +/** + * Request to derive a bunch of CS @a r_pubs using the + * denominations and nonces from @a cdds. + * + * @param cdds array to compute @a r_pubs from + * @param cdds_length length of the @a cdds array + * @param for_melt true if this is for a melt operation + * @param[out] r_pubs array where to write the result; must be of length @a cdds_length + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub_simple ( + unsigned int cdds_length, + const struct TEH_CsDeriveData cdds[static cdds_length], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]); + + +/** + * Request to derive a bunch of CS @a r_pubs using the + * denominations and nonces from @a cdds. + * + * @param ksh keys state to load the keys from + * @param num number of input elements + * @param h_denom_pubs array @a num of hashes of keys to sign with + * @param nonces array @a num of nonces to use + * @param for_melt true if this is for a melt operation + * @param[out] r_pubs array where to write the result; must be of length @a num + * @param[out] err_idx in case of error, the index into @e cdds that caused it + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub ( + const struct TEH_KeyStateHandle *ksh, + size_t num, + const struct TALER_DenominationHashP h_denom_pubs[static num], + const struct GNUNET_CRYPTO_CsSessionNonce nonces[static num], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static num], + size_t *err_idx); + +/** + * Revoke the public key associated with @a h_denom_pub. + * This function should be called AFTER the database was + * updated, as it also triggers #TEH_keys_update_states(). + * + * Note that the actual revocation happens asynchronously and + * may thus fail silently. To verify that the revocation succeeded, + * clients must watch for the associated change to the key state. + * + * @param h_denom_pub hash of the public key to revoke + */ +void +TEH_keys_denomination_revoke ( + const struct TALER_DenominationHashP *h_denom_pub); + + +/** + * Fully clean up keys subsystem. + */ +void +TEH_keys_finished (void); + + +/** + * Resumes all suspended /keys requests, we may now have key material + * (or are shutting down). + * + * @param do_shutdown are we shutting down? + */ +void +TEH_resume_keys_requests (bool do_shutdown); + + +/** + * Sign the message in @a purpose with the exchange's signing key. + * + * The @a purpose data is the beginning of the data of which the signature is + * to be created. The `size` field in @a purpose must correctly indicate the + * number of bytes of the data structure, including its header. Use + * #TEH_keys_exchange_sign() instead of calling this function directly! + * + * @param purpose the message to sign + * @param[out] pub set to the current public signing key of the exchange + * @param[out] sig signature over purpose using current signing key + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_exchange_sign_ ( + const struct GNUNET_CRYPTO_SignaturePurpose *purpose, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig); + + +/** + * Sign the message in @a purpose with the exchange's signing key. + * + * The @a purpose data is the beginning of the data of which the signature is + * to be created. The `size` field in @a purpose must correctly indicate the + * number of bytes of the data structure, including its header. Use + * #TEH_keys_exchange_sign() instead of calling this function directly! + * + * @param cls key state state to look in + * @param purpose the message to sign + * @param[out] pub set to the current public signing key of the exchange + * @param[out] sig signature over purpose using current signing key + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_exchange_sign2_ ( + void *cls, + const struct GNUNET_CRYPTO_SignaturePurpose *purpose, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig); + + +/** + * @ingroup crypto + * @brief EdDSA sign a given block. + * + * The @a ps data must be a fixed-size struct for which the signature is to be + * created. The `size` field in @a ps->purpose must correctly indicate the + * number of bytes of the data structure, including its header. + * + * @param ps packed struct with what to sign, MUST begin with a purpose + * @param[out] pub where to store the public key to use for the signing + * @param[out] sig where to write the signature + * @return #TALER_EC_NONE on success + */ +#define TEH_keys_exchange_sign(ps,pub,sig) \ + ({ \ + /* check size is set correctly */ \ + GNUNET_assert (htonl ((ps)->purpose.size) == \ + sizeof (*ps)); \ + /* check 'ps' begins with the purpose */ \ + GNUNET_static_assert (((void*) (ps)) == \ + ((void*) &(ps)->purpose)); \ + TEH_keys_exchange_sign_ (&(ps)->purpose, \ + pub, \ + sig); \ + }) + + +/** + * @ingroup crypto + * @brief EdDSA sign a given block. + * + * The @a ps data must be a fixed-size struct for which the signature is to be + * created. The `size` field in @a ps->purpose must correctly indicate the + * number of bytes of the data structure, including its header. + * + * This allows requesting multiple denominations with the same @a ksh which + * thus will remain valid until the next call to + * #TEH_keys_denomination_by_hash() or #TEH_keys_get_state() or + * #TEH_keys_exchange_sign(). + * + * @param ksh key state to use + * @param ps packed struct with what to sign, MUST begin with a purpose + * @param[out] pub where to store the public key to use for the signing + * @param[out] sig where to write the signature + * @return #TALER_EC_NONE on success + */ +#define TEH_keys_exchange_sign2(ksh,ps,pub,sig) \ + ({ \ + /* check size is set correctly */ \ + GNUNET_assert (htonl ((ps)->purpose.size) == \ + sizeof (*ps)); \ + /* check 'ps' begins with the purpose */ \ + GNUNET_static_assert (((void*) (ps)) == \ + ((void*) &(ps)->purpose)); \ + TEH_keys_exchange_sign2_ (ksh, \ + &(ps)->purpose, \ + pub, \ + sig); \ + }) + + +/** + * Revoke the given exchange's signing key. + * This function should be called AFTER the database was + * updated, as it also triggers #TEH_keys_update_states(). + * + * Note that the actual revocation happens asynchronously and + * may thus fail silently. To verify that the revocation succeeded, + * clients must watch for the associated change to the key state. + * + * @param exchange_pub key to revoke + */ +void +TEH_keys_exchange_revoke (const struct TALER_ExchangePublicKeyP *exchange_pub); + + +/** + * Function to call to handle requests to "/keys" by sending + * back our current key material. + * + * @param rc request context + * @param args array of additional options (must be empty for this function) + * @return MHD result code + */ +MHD_RESULT +TEH_keys_get_handler (struct TEH_RequestContext *rc, + const char *const args[]); + + +/** + * Function to call to handle requests to "/management/keys" by sending + * back our future key material. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, + struct MHD_Connection *connection); + + +/** + * Load fees and expiration times (!) for the denomination type configured for + * the denomination matching @a h_denom_pub. + * + * @param ksh key state to load fees from + * @param h_denom_pub hash of the denomination public key + * to use to derive the section name of the configuration to use + * @param[out] denom_pub set to the denomination public key (to be freed by caller!) + * @param[out] meta denomination type data to complete + * @return #GNUNET_OK on success, + * #GNUNET_NO if @a h_denom_pub is not known + * #GNUNET_SYSERR on hard errors + */ +enum GNUNET_GenericReturnValue +TEH_keys_load_fees (struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *h_denom_pub, + struct TALER_DenominationPublicKey *denom_pub, + struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta); + + +/** + * Load expiration times for the given onling signing key. + * + * @param exchange_pub the online signing key + * @param[out] meta set to meta data about the key + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TEH_keys_get_timing (const struct TALER_ExchangePublicKeyP *exchange_pub, + struct TALER_EXCHANGEDB_SignkeyMetaData *meta); + + +/** + * Initialize keys subsystem. + * + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TEH_keys_init (void); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.c b/src/exchange/taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.c @@ -0,0 +1,506 @@ +/* + This file is part of TALER + Copyright (C) 2021-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.c + * @brief Handle request for generic KYC check. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.h" +#include "taler-exchange-httpd_post-kyc-wallet.h" +#include "taler-exchange-httpd_responses.h" + +/** + * Reserve GET request that is long-polling. + */ +struct KycPoller +{ + /** + * Kept in a DLL. + */ + struct KycPoller *next; + + /** + * Kept in a DLL. + */ + struct KycPoller *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Subscription for the database event we are + * waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Account for which we perform the KYC check. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * When will this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Signature by the account owner authorizing this + * operation. + */ + union TALER_AccountSignatureP account_sig; + + /** + * Public key from the account owner authorizing this + * operation. Optional, see @e have_pub. + */ + union TALER_AccountPublicKeyP account_pub; + + /** + * Generation of KYC rules already known to the client + * (when long-polling). Do not send these rules again. + */ + uint64_t min_rule; + + /** + * What are we long-polling for (if anything)? + */ + enum TALER_EXCHANGE_KycLongPollTarget lpt; + + /** + * True if we are still suspended. + */ + bool suspended; + + /** + * True if we have an @e account_pub. + */ + bool have_pub; + +}; + + +/** + * Head of list of requests in long polling. + */ +static struct KycPoller *kyp_head; + +/** + * Tail of list of requests in long polling. + */ +static struct KycPoller *kyp_tail; + + +void +TEH_kyc_check_cleanup () +{ + struct KycPoller *kyp; + + while (NULL != (kyp = kyp_head)) + { + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + if (kyp->suspended) + { + kyp->suspended = false; + MHD_resume_connection (kyp->connection); + } + } +} + + +/** + * Function called once a connection is done to + * clean up the `struct ReservePoller` state. + * + * @param rc context to clean up for + */ +static void +kyp_cleanup (struct TEH_RequestContext *rc) +{ + struct KycPoller *kyp = rc->rh_ctx; + + GNUNET_assert (! kyp->suspended); + if (NULL != kyp->eh) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancelling DB event listening (KYP cleanup)\n"); + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + kyp->eh); + kyp->eh = NULL; + } + GNUNET_free (kyp); +} + + +/** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct TEH_RequestContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct TEH_RequestContext *rc = cls; + struct KycPoller *kyp = rc->rh_ctx; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + if (! kyp->suspended) + return; /* event triggered while main transaction + was still running, or got multiple wake-up events */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received KYC update event\n"); + kyp->suspended = false; + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); + TEH_check_invariants (); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming from long-polling on KYC status\n"); + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + MHD_resume_connection (kyp->connection); + TALER_MHD_daemon_trigger (); + TEH_check_invariants (); + GNUNET_async_scope_restore (&old_scope); +} + + +MHD_RESULT +TEH_handler_kyc_check ( + struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct KycPoller *kyp = rc->rh_ctx; + json_t *jrules = NULL; + json_t *jlimits = NULL; + union TALER_AccountPublicKeyP reserve_pub; + struct TALER_AccountAccessTokenP access_token; + bool aml_review; + bool kyc_required; + bool access_ok = false; + enum GNUNET_GenericReturnValue is_wallet; + uint64_t rule_gen = 0; + + if (NULL == kyp) + { + bool sig_required = true; + + kyp = GNUNET_new (struct KycPoller); + kyp->connection = rc->connection; + rc->rh_ctx = kyp; + rc->rh_cleaner = &kyp_cleanup; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &kyp->h_payto, + sizeof (kyp->h_payto))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PATH_SEGMENT_MALFORMED, + "h_payto"); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking KYC status for normalized payto hash %s\n", + args[0]); + TALER_MHD_parse_request_header_auto ( + rc->connection, + TALER_HTTP_HEADER_ACCOUNT_OWNER_SIGNATURE, + &kyp->account_sig, + sig_required); + TALER_MHD_parse_request_header_auto ( + rc->connection, + TALER_HTTP_HEADER_ACCOUNT_OWNER_PUBKEY, + &kyp->account_pub, + kyp->have_pub); + TALER_MHD_parse_request_timeout (rc->connection, + &kyp->timeout); + { + uint64_t num = 0; + int val; + + TALER_MHD_parse_request_number (rc->connection, + "lpt", + &num); + val = (int) num; + if ( (val < 0) || + (val > TALER_EXCHANGE_KLPT_MAX) ) + { + /* Protocol violation, but we can be graceful and + just ignore the long polling! */ + GNUNET_break_op (0); + val = TALER_EXCHANGE_KLPT_NONE; + } + kyp->lpt = (enum TALER_EXCHANGE_KycLongPollTarget) val; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Long polling for target %d with timeout %s\n", + val, + GNUNET_TIME_relative2s ( + GNUNET_TIME_absolute_get_remaining ( + kyp->timeout), + true)); + } + TALER_MHD_parse_request_number (rc->connection, + "min_rule", + &kyp->min_rule); + /* long polling needed? */ + if (GNUNET_TIME_absolute_is_future (kyp->timeout)) + { + struct TALER_KycCompletedEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED), + .h_payto = kyp->h_payto + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting DB event listening for KYC COMPLETED\n"); + kyp->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (kyp->timeout), + &rep.header, + &db_event_cb, + rc); + } + } /* end initialization */ + + if (! TEH_enable_kyc) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC not enabled\n"); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + + { + enum GNUNET_DB_QueryStatus qs; + bool do_suspend; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Looking up KYC requirements for account %s (%s)\n", + TALER_B2S (&kyp->h_payto), + kyp->have_pub ? "with account-pub" : "legacy wallet"); + qs = TEH_plugin->lookup_kyc_requirement_by_row ( + TEH_plugin->cls, + &kyp->h_payto, + kyp->have_pub, + &kyp->account_pub, + &is_wallet, + &reserve_pub.reserve_pub, + &access_token, + &rule_gen, + &jrules, + &aml_review, + &kyc_required); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_DB_STORE_FAILED, + "lookup_kyc_requirement_by_row"); + } + + do_suspend = false; + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* account unknown */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Account unknown!\n"); + if ( (TALER_EXCHANGE_KLPT_NONE != kyp->lpt) && + (TALER_EXCHANGE_KLPT_KYC_OK != kyp->lpt) && + (GNUNET_TIME_absolute_is_future (kyp->timeout)) ) + { + do_suspend = true; + access_ok = true; /* for now */ + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found rule %llu (client wants > %llu)\n", + (unsigned long long) rule_gen, + (unsigned long long) kyp->min_rule); + access_ok = + ( (! GNUNET_is_zero (&kyp->account_pub) && + (GNUNET_OK == + TALER_account_kyc_auth_verify (&kyp->account_pub, + &kyp->account_sig)) ) || + (! GNUNET_is_zero (&reserve_pub) && + (GNUNET_OK == + TALER_account_kyc_auth_verify (&reserve_pub, + &kyp->account_sig)) ) ); + if (GNUNET_TIME_absolute_is_future (kyp->timeout) && + (rule_gen <= kyp->min_rule) ) + { + switch (kyp->lpt) + { + case TALER_EXCHANGE_KLPT_NONE: + /* If KLPT is not given, just go for rule generation */ + do_suspend = true; + break; + case TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER: + if (! access_ok) + do_suspend = true; + break; + case TALER_EXCHANGE_KLPT_INVESTIGATION_DONE: + if (! aml_review) + do_suspend = true; + break; + case TALER_EXCHANGE_KLPT_KYC_OK: + if (kyc_required) + do_suspend = true; + break; + } + } + } + + if (do_suspend && + (access_ok || + (TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER == kyp->lpt) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending HTTP request on timeout (%s) for %d\n", + GNUNET_TIME_relative2s (GNUNET_TIME_absolute_get_remaining ( + kyp->timeout), + true), + (int) kyp->lpt); + GNUNET_assert (NULL != kyp->eh); + kyp->suspended = true; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (kyp->connection); + return MHD_YES; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning account unknown\n"); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN, + NULL); + } + } + + if (! access_ok) + { + json_decref (jrules); + jrules = NULL; + if (GNUNET_is_zero (&kyp->account_pub)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_KEY_UNKNOWN, + NULL); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning authorization failed\n"); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED), + GNUNET_JSON_pack_data_auto ("expected_account_pub", + &kyp->account_pub)); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC rules apply to %s:\n", + (GNUNET_SYSERR == is_wallet) + ? "unknown account type" + : ( (GNUNET_YES == is_wallet) + ? "wallet" + : "account")); + if (NULL != jrules) + json_dumpf (jrules, + stderr, + JSON_INDENT (2)); + + jlimits = TALER_KYCLOGIC_rules_to_limits (jrules, + is_wallet); + if (NULL == jlimits) + { + GNUNET_break_op (0); + json_decref (jrules); + jrules = NULL; + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "/kyc-check: rules_to_limits failed"); + } + json_decref (jrules); + jrules = NULL; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC limits apply:\n"); + json_dumpf (jlimits, + stderr, + JSON_INDENT (2)); + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning KYC %s\n", + kyc_required ? "required" : "optional"); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + kyc_required + ? MHD_HTTP_ACCEPTED + : MHD_HTTP_OK, + GNUNET_JSON_pack_bool ("aml_review", + aml_review), + GNUNET_JSON_pack_uint64 ("rule_gen", + rule_gen), + GNUNET_JSON_pack_data_auto ("access_token", + &access_token), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("limits", + jlimits))); +} + + +/* end of taler-exchange-httpd_kyc-check.c */ diff --git a/src/exchange/taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.h b/src/exchange/taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + Copyright (C) 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-kyc-check-H_NORMALIZED_PAYTO.h + * @brief Handle /kyc-check requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_KYC_CHECK_H_NORMALIZED_PAYTO_H +#define TALER_EXCHANGE_HTTPD_GET_KYC_CHECK_H_NORMALIZED_PAYTO_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/kyc-check" request. Checks the KYC + * status of the given account and returns it. + * + * @param rc details about the request to handle + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_check ( + struct TEH_RequestContext *rc, + const char *const args[1]); + + +/** + * Clean up long-polling KYC requests during shutdown. + */ +void +TEH_kyc_check_cleanup (void); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.c b/src/exchange/taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.c @@ -0,0 +1,825 @@ +/* + This file is part of TALER + Copyright (C) 2021-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.c + * @brief Handle request for generic KYC info. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_exchangedb_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_common_kyc.h" + + +/** + * Context for the GET /kyc-info request. + * + * Used for long-polling and other asynchronous waiting. + */ +struct KycPoller +{ + /** + * Kept in a DLL. + */ + struct KycPoller *next; + + /** + * Kept in a DLL. + */ + struct KycPoller *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Subscription for the database event we are + * waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Handle to async activity to get the latest legitimization + * rule set. + */ + struct TALER_EXCHANGEDB_RuleUpdater *ru; + + /** + * Current legitimization rule set, owned by callee. Will be NULL on error + * or for default rules. Will not contain skip rules and not be expired. + */ + struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs; + + /** + * Handle for async KYC processing. + */ + struct TEH_KycMeasureRunContext *kat; + + /** + * Response to return, NULL if none yet. + */ + struct MHD_Response *response; + + /** + * Set to access token for a KYC process by the account, + * if @e have_token is true. + */ + struct TALER_AccountAccessTokenP access_token; + + /** + * Payto hash of the account matching @a access_token. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * #MHD_HTTP_HEADER_IF_NONE_MATCH Etag value sent by the client. 0 for none + * (or malformed). + */ + uint64_t etag_outcome_in; + + /** + * #MHD_HTTP_HEADER_IF_NONE_MATCH Etag value sent by the client. 0 for none + * (or malformed). + */ + uint64_t etag_measure_in; + + /** + * When will this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * + */ + uint64_t legitimization_measure_last_row; + + /** + * Row in the legitimization outcomes table that @e lrs matches. + */ + uint64_t legitimization_outcome_last_row; + + /** + * HTTP status code to use with @e response. + */ + unsigned int response_code; + + /** + * #GNUNET_YES if our @e h_payto is for a wallet, + * #GNUNET_NO if it is for an account, + * #GNUNET_SYSERR if we do not know. + */ + enum GNUNET_GenericReturnValue is_wallet; + + /** + * True if we are still suspended. + */ + bool suspended; + +}; + + +/** + * Head of list of requests in long polling. + */ +static struct KycPoller *kyp_head; + +/** + * Tail of list of requests in long polling. + */ +static struct KycPoller *kyp_tail; + + +void +TEH_kyc_info_cleanup () +{ + struct KycPoller *kyp; + + while (NULL != (kyp = kyp_head)) + { + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + if (kyp->suspended) + { + kyp->suspended = false; + MHD_resume_connection (kyp->connection); + } + } +} + + +/** + * Function called once a connection is done to + * clean up the `struct ReservePoller` state. + * + * @param rc context to clean up for + */ +static void +kyp_cleanup (struct TEH_RequestContext *rc) +{ + struct KycPoller *kyp = rc->rh_ctx; + + GNUNET_assert (! kyp->suspended); + if (NULL != kyp->eh) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancelling DB event listening\n"); + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + kyp->eh); + kyp->eh = NULL; + } + if (NULL != kyp->ru) + { + TALER_EXCHANGEDB_update_rules_cancel (kyp->ru); + kyp->ru = NULL; + } + if (NULL != kyp->response) + { + MHD_destroy_response (kyp->response); + kyp->response = NULL; + } + if (NULL != kyp->lrs) + { + TALER_KYCLOGIC_rules_free (kyp->lrs); + kyp->lrs = NULL; + } + if (NULL != kyp->kat) + { + TEH_kyc_run_measure_cancel (kyp->kat); + kyp->kat = NULL; + } + GNUNET_free (kyp); +} + + +/** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct TEH_RequestContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct TEH_RequestContext *rc = cls; + struct KycPoller *kyp = rc->rh_ctx; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + if (! kyp->suspended) + return; /* event triggered while main transaction + was still running, or got multiple wake-up events */ + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming from long-polling on KYC status\n"); + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + kyp->suspended = false; + MHD_resume_connection (kyp->connection); + TALER_MHD_daemon_trigger (); + GNUNET_async_scope_restore (&old_scope); +} + + +/** + * Add the headers we want to set for every response. + * + * @param[in,out] response the response to modify + */ +static void +add_nocache_header (struct MHD_Response *response) +{ + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-cache")); +} + + +/** + * Resume processing the @a kyp request with the @a response. + * + * @param kyp request to resume and respond to + * @param http_status HTTP status for @a response + * @param response HTTP response to return + */ +static void +resume_with_response (struct KycPoller *kyp, + unsigned int http_status, + struct MHD_Response *response) +{ + kyp->response_code = http_status; + kyp->response = response; + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + kyp->suspended = false; + MHD_resume_connection (kyp->connection); + TALER_MHD_daemon_trigger (); +} + + +/** + * Function called after a measure has been run. + * + * @param kyp request to fail with the error code + * @param ec error code or 0 on success + * @param hint detail error message or NULL on success / no info + */ +static void +fail_with_ec ( + struct KycPoller *kyp, + enum TALER_ErrorCode ec, + const char *hint) +{ + resume_with_response (kyp, + TALER_ErrorCode_get_http_status (ec), + TALER_MHD_make_error (ec, + hint)); +} + + +/** + * Generate a reply with the KycProcessClientInformation from + * the LegitimizationMeasures. + * + * @param[in,out] kyp request to reply on + * @param legitimization_measure_row_id part of etag to set for the response + * @param legitimization_outcome_row_id part of etag to set for the response + * @param jmeasures a `LegitimizationMeasures` object to encode + * @param jvoluntary array of voluntary measures to encode, can be NULL + */ +static void +resume_with_reply (struct KycPoller *kyp, + uint64_t legitimization_measure_row_id, + uint64_t legitimization_outcome_row_id, + const json_t *jmeasures, + const json_t *jvoluntary) +{ + const json_t *measures; /* array of MeasureInformation */ + bool is_and_combinator = false; + bool verboten; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("is_and_combinator", + &is_and_combinator), + NULL), + GNUNET_JSON_spec_bool ("verboten", + &verboten), + GNUNET_JSON_spec_array_const ("measures", + &measures), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + const char *ename; + unsigned int eline; + json_t *kris; + size_t i; + json_t *mi; /* a MeasureInformation object */ + + ret = GNUNET_JSON_parse (jmeasures, + spec, + &ename, + &eline); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + fail_with_ec (kyp, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + ename); + return; + } + kris = json_array (); + GNUNET_assert (NULL != kris); + json_array_foreach ((json_t *) measures, i, mi) + { + const char *check_name; + const json_t *context = NULL; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("check_name", + &check_name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("context", + &context), + NULL), + GNUNET_JSON_spec_end () + }; + json_t *kri; + + ret = GNUNET_JSON_parse (mi, + ispec, + &ename, + &eline); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + json_decref (kris); + fail_with_ec ( + kyp, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + ename); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found required check `%s'\n", + check_name); + if (NULL != context) + { + json_dumpf (context, + stderr, + JSON_INDENT (2)); + fprintf (stderr, + "\n"); + } + /* Check if requirement is a duplicate, and in that case do + not return it */ + { + bool duplicate = false; + + for (size_t off = 0; off < i; off++) + { + json_t *have = json_array_get (measures, + off); + if ( (1 == + json_equal (json_object_get (have, + "check_name"), + json_object_get (mi, + "check_name")) ) && + (1 == + json_equal (json_object_get (have, + "check_name"), + json_object_get (mi, + "check_name")) ) && + (1 == + json_equal (json_object_get (have, + "check_name"), + json_object_get (mi, + "check_name")) ) ) + { + /* Duplicate requirement, do not return again */ + duplicate = true; + break; + } + } + if (duplicate) + { + continue; + } + } + kri = TALER_KYCLOGIC_measure_to_requirement ( + check_name, + context, + &kyp->access_token, + i, + legitimization_measure_row_id); + if (NULL == kri) + { + GNUNET_break (0); + json_decref (kris); + fail_with_ec ( + kyp, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "could not convert measure to requirement"); + return; + } + GNUNET_assert (0 == + json_array_append_new (kris, + kri)); + } + + { + char etags[128]; + struct MHD_Response *resp; + + GNUNET_snprintf (etags, + sizeof (etags), + "\"%llu-%llu\"", + (unsigned long long) legitimization_measure_row_id, + (unsigned long long) legitimization_outcome_row_id); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("requirements", + kris), + GNUNET_JSON_pack_bool ("is_and_combinator", + is_and_combinator), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ( + "voluntary_measures", + (json_t *) jvoluntary))); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etags)); + add_nocache_header (resp); + resume_with_response (kyp, + MHD_HTTP_OK, + resp); + } +} + + +/** + * Function called with the current rule set. + * + * @param cls closure with a `struct KycPoller *` + * @param rur includes legitimziation rule set that applies to the account + * (owned by callee, callee must free the lrs!) + */ +static void +current_rules_cb ( + void *cls, + struct TALER_EXCHANGEDB_RuleUpdaterResult *rur) +{ + struct KycPoller *kyp = cls; + enum GNUNET_DB_QueryStatus qs; + uint64_t legitimization_measure_last_row; + json_t *jmeasures; + json_t *vmeasures; + + kyp->ru = NULL; + if (TALER_EC_NONE != rur->ec) + { + /* Rollback should not be needed, just to be sure */ + TEH_plugin->rollback (TEH_plugin->cls); + fail_with_ec (kyp, + rur->ec, + rur->hint); + return; + } + GNUNET_assert (NULL == kyp->lrs); + kyp->lrs + = rur->lrs; + kyp->legitimization_outcome_last_row + = rur->legitimization_outcome_last_row; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "LRS is for the account uses %s\n", + NULL == kyp->lrs + ? "default rules" + : "custom rules"); + + /* Check if there is an unfinished legitimization measure */ + qs = TEH_plugin->lookup_kyc_status_by_token ( + TEH_plugin->cls, + &kyp->access_token, + &legitimization_measure_last_row, + &jmeasures); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "lookup_kyc_status_by_token returned %d\n", + (int) qs); + if (qs < 0) + { + GNUNET_break (0); + TEH_plugin->rollback (TEH_plugin->cls); + fail_with_ec ( + kyp, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_kyc_status_by_token"); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + jmeasures + = TALER_KYCLOGIC_zero_measures (kyp->lrs, + kyp->is_wallet); + if (NULL == jmeasures) + { + qs = TEH_plugin->commit (TEH_plugin->cls); + if (qs < 0) + { + TEH_plugin->rollback (TEH_plugin->cls); + fail_with_ec ( + kyp, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "kyc-info"); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No KYC requirement open\n"); + resume_with_response (kyp, + MHD_HTTP_OK, + TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ( + "voluntary_measures", + TALER_KYCLOGIC_voluntary_measures ( + kyp->lrs)) + ))); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Making applicable zero-measures for %s under current rules active\n", + (GNUNET_SYSERR == kyp->is_wallet) + ? "unknown account type" + : ( (GNUNET_YES == kyp->is_wallet) + ? "wallets" + : "accounts")); + json_dumpf (jmeasures, + stderr, + JSON_INDENT (2)); + qs = TEH_plugin->insert_active_legitimization_measure ( + TEH_plugin->cls, + &kyp->access_token, + jmeasures, + &legitimization_measure_last_row); + if (qs < 0) + { + GNUNET_break (0); + TEH_plugin->rollback (TEH_plugin->cls); + fail_with_ec (kyp, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_active_legitimization_measure"); + return; + } + } + if ( (legitimization_measure_last_row == kyp->etag_measure_in) && + (kyp->legitimization_outcome_last_row == kyp->etag_outcome_in) && + GNUNET_TIME_absolute_is_future (kyp->timeout) ) + { + /* Note: in practice this commit should do nothing, but we cannot + trust that the client provided correct etags, and so we must + commit anyway just in case the client lied about the etags. */ + qs = TEH_plugin->commit (TEH_plugin->cls); + if (qs < 0) + { + TEH_plugin->rollback (TEH_plugin->cls); + fail_with_ec ( + kyp, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "kyc-info"); + return; + } + if (NULL != kyp->lrs) + { + TALER_KYCLOGIC_rules_free (kyp->lrs); + kyp->lrs = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending HTTP request on timeout (%s)\n", + GNUNET_TIME_relative2s ( + GNUNET_TIME_absolute_get_remaining ( + kyp->timeout), + true)); + GNUNET_assert (NULL != kyp->eh); + GNUNET_break (kyp->suspended); + return; + } + qs = TEH_plugin->commit (TEH_plugin->cls); + if (qs < 0) + { + TEH_plugin->rollback (TEH_plugin->cls); + fail_with_ec ( + kyp, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "kyc-info"); + return; + } + if ( (legitimization_measure_last_row == + kyp->etag_measure_in) && + (kyp->legitimization_outcome_last_row == + kyp->etag_outcome_in) ) + { + char etags[128]; + struct MHD_Response *resp; + + GNUNET_snprintf (etags, + sizeof (etags), + "\"%llu-%llu\"", + (unsigned long long) legitimization_measure_last_row, + (unsigned long long) kyp->legitimization_outcome_last_row); + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + add_nocache_header (resp); + TALER_MHD_add_global_headers (resp, + false); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etags)); + resume_with_response (kyp, + MHD_HTTP_NOT_MODIFIED, + resp); + json_decref (jmeasures); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Generating success reply to kyc-info query\n"); + vmeasures = TALER_KYCLOGIC_voluntary_measures (kyp->lrs); + resume_with_reply (kyp, + legitimization_measure_last_row, + kyp->legitimization_outcome_last_row, + jmeasures, + vmeasures); + json_decref (vmeasures); + json_decref (jmeasures); +} + + +MHD_RESULT +TEH_handler_kyc_info ( + struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct KycPoller *kyp = rc->rh_ctx; + enum GNUNET_DB_QueryStatus qs; + + if (NULL == kyp) + { + bool bis_wallet; + + kyp = GNUNET_new (struct KycPoller); + kyp->connection = rc->connection; + rc->rh_ctx = kyp; + rc->rh_cleaner = &kyp_cleanup; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + args[0], + strlen (args[0]), + &kyp->access_token, + sizeof (kyp->access_token))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "access token"); + } + TALER_MHD_parse_request_timeout (rc->connection, + &kyp->timeout); + + /* Get etag */ + { + const char *etags; + + etags = MHD_lookup_connection_value ( + rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != etags) + { + char dummy; + unsigned long long ev1; + unsigned long long ev2; + + if (2 != sscanf (etags, + "\"%llu-%llu\"%c", + &ev1, + &ev2, + &dummy)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client send malformed `%s' header `%s'\n", + MHD_HTTP_HEADER_IF_NONE_MATCH, + etags); + } + else + { + kyp->etag_measure_in = (uint64_t) ev1; + kyp->etag_outcome_in = (uint64_t) ev2; + } + } + } /* etag */ + + /* Check access token */ + kyp->is_wallet = GNUNET_SYSERR; + qs = TEH_plugin->lookup_h_payto_by_access_token ( + TEH_plugin->cls, + &kyp->access_token, + &kyp->h_payto, + &bis_wallet); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_h_payto_by_access_token"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break_op (0); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_INFO_AUTHORIZATION_FAILED)); + } + kyp->is_wallet = (bis_wallet) ? GNUNET_YES : GNUNET_NO; + + if (GNUNET_TIME_absolute_is_future (kyp->timeout)) + { + struct TALER_KycCompletedEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED), + .h_payto = kyp->h_payto + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting DB event listening\n"); + kyp->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (kyp->timeout), + &rep.header, + &db_event_cb, + rc); + } + } /* end of one-time initialization */ + + if (NULL != kyp->response) + { + return MHD_queue_response (rc->connection, + kyp->response_code, + kyp->response); + } + + kyp->ru = TALER_EXCHANGEDB_update_rules (TEH_plugin, + &TEH_attribute_key, + &kyp->h_payto, + kyp->is_wallet, + &current_rules_cb, + kyp); + kyp->suspended = true; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (rc->connection); + return MHD_YES; + +} + + +/* end of taler-exchange-httpd_kyc-info.c */ diff --git a/src/exchange/taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.h b/src/exchange/taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + Copyright (C) 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-kyc-info-ACCESS_TOKEN.h + * @brief Handle /kyc-info requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_KYC_INFO_ACCESS_TOKEN_H +#define TALER_EXCHANGE_HTTPD_GET_KYC_INFO_ACCESS_TOKEN_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown kyc-info subsystem. Resumes all suspended long-polling clients + * and cleans up data structures. + */ +void +TEH_kyc_info_cleanup (void); + + +/** + * Handle a "/kyc-info" request. + * + * @param rc request to handle + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_info ( + struct TEH_RequestContext *rc, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.c b/src/exchange/taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.c @@ -0,0 +1,641 @@ +/* + This file is part of TALER + Copyright (C) 2021-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.c + * @brief Handle request for proof for KYC check. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_attributes.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_templating_lib.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Context for the proof. + */ +struct KycProofContext +{ + + /** + * Kept in a DLL while suspended. + */ + struct KycProofContext *next; + + /** + * Kept in a DLL while suspended. + */ + struct KycProofContext *prev; + + /** + * Details about the connection we are processing. + */ + struct TEH_RequestContext *rc; + + /** + * Proof logic to run. + */ + struct TALER_KYCLOGIC_Plugin *logic; + + /** + * Configuration for @a logic. + */ + struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Asynchronous operation with the proof system. + */ + struct TALER_KYCLOGIC_ProofHandle *ph; + + /** + * KYC AML trigger operation. + */ + struct TEH_KycMeasureRunContext *kat; + + /** + * Process information about the user for the plugin from the database, can + * be NULL. + */ + char *provider_user_id; + + /** + * Process information about the legitimization process for the plugin from the + * database, can be NULL. + */ + char *provider_legitimization_id; + + /** + * Hash of payment target URI this is about. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * Final HTTP response to return. + */ + struct MHD_Response *response; + + /** + * Final HTTP response code to return. + */ + unsigned int response_code; + + /** + * HTTP response from the KYC provider plugin. + */ + struct MHD_Response *proof_response; + + /** + * HTTP response code from the KYC provider plugin. + */ + unsigned int proof_response_code; + + /** + * Provider configuration section name of the logic we are running. + */ + const char *provider_name; + + /** + * Row in the database for this legitimization operation. + */ + uint64_t process_row; + + /** + * True if we are suspended, + */ + bool suspended; + + /** + * True if @e h_payto is for a wallet. + */ + bool is_wallet; + +}; + + +/** + * Contexts are kept in a DLL while suspended. + */ +static struct KycProofContext *kpc_head; + +/** + * Contexts are kept in a DLL while suspended. + */ +static struct KycProofContext *kpc_tail; + + +/** + * Generate HTML error for @a connection using @a template. + * + * @param connection HTTP client connection + * @param template template to expand + * @param[in,out] http_status HTTP status of the response + * @param ec Taler error code to return + * @param message extended message to return + * @return MHD response object + */ +static struct MHD_Response * +make_html_error (struct MHD_Connection *connection, + const char *template, + unsigned int *http_status, + enum TALER_ErrorCode ec, + const char *message) +{ + struct MHD_Response *response = NULL; + json_t *body; + enum GNUNET_GenericReturnValue ret; + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("message", + message)), + TALER_JSON_pack_ec ( + ec)); + ret = TALER_TEMPLATING_build (connection, + http_status, + template, + NULL, + NULL, + body, + &response); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_SYSERR != ret) + GNUNET_break (MHD_NO != + MHD_add_response_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/html")); + + json_decref (body); + return response; +} + + +/** + * Resume processing the @a kpc request. + * + * @param kpc request to resume + */ +static void +kpc_resume (struct KycProofContext *kpc) +{ + GNUNET_assert (GNUNET_YES == kpc->suspended); + kpc->suspended = false; + GNUNET_CONTAINER_DLL_remove (kpc_head, + kpc_tail, + kpc); + MHD_resume_connection (kpc->rc->connection); + TALER_MHD_daemon_trigger (); +} + + +void +TEH_kyc_proof_cleanup (void) +{ + struct KycProofContext *kpc; + + while (NULL != (kpc = kpc_head)) + { + if (NULL != kpc->ph) + { + kpc->logic->proof_cancel (kpc->ph); + kpc->ph = NULL; + } + kpc_resume (kpc); + } +} + + +/** + * Function called after the KYC-AML trigger is done. + * + * @param cls closure + * @param ec error code or 0 on success + * @param detail error message or NULL on success / no info + */ +static void +proof_finish ( + void *cls, + enum TALER_ErrorCode ec, + const char *detail) +{ + struct KycProofContext *kpc = cls; + + kpc->kat = NULL; + if (TALER_EC_NONE != ec) + { + kpc->response_code = TALER_ErrorCode_get_http_status (ec); + GNUNET_break (5 != kpc->response_code / 100); + GNUNET_assert (kpc->response_code != UINT_MAX); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Templating error response for %d and HTTP status %u (%s)\n", + (int) ec, + kpc->response_code, + detail); + kpc->response = make_html_error ( + kpc->rc->connection, + "kyc-proof-internal-error", + &kpc->response_code, + ec, + detail); + } + else + { + GNUNET_assert (NULL != kpc->proof_response); + kpc->response_code = kpc->proof_response_code; + kpc->response = kpc->proof_response; + kpc->proof_response = NULL; + kpc->proof_response_code = 0; + } + GNUNET_assert (NULL != kpc->response); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming with response %p and status %u\n", + kpc->response, + kpc->response_code); + kpc_resume (kpc); +} + + +/** + * Respond with an HTML message on the given @a rc. + * + * @param[in,out] rc request to respond to + * @param http_status HTTP status code to use + * @param template template to fill in + * @param ec error code to use for the template + * @param message additional message to return + * @return MHD result code + */ +static MHD_RESULT +respond_html_ec (struct TEH_RequestContext *rc, + unsigned int http_status, + const char *template, + enum TALER_ErrorCode ec, + const char *message) +{ + struct MHD_Response *response; + MHD_RESULT res; + + response = make_html_error (rc->connection, + template, + &http_status, + ec, + message); + res = MHD_queue_response (rc->connection, + http_status, + response); + MHD_destroy_response (response); + return res; +} + + +/** + * Function called with the result of a proof check operation. + * + * Note that the "decref" for the @a response + * will be done by the callee and MUST NOT be done by the plugin. + * + * @param cls closure + * @param status KYC status + * @param provider_name name of the provider + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client + */ +static void +proof_cb ( + void *cls, + enum TALER_KYCLOGIC_KycStatus status, + const char *provider_name, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response) +{ + struct KycProofContext *kpc = cls; + struct TEH_RequestContext *rc = kpc->rc; + struct GNUNET_AsyncScopeSave old_scope; + enum GNUNET_DB_QueryStatus qs; + + kpc->ph = NULL; + kpc->proof_response = response; + kpc->proof_response_code = http_status; + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); + switch (status) + { + case TALER_KYCLOGIC_STATUS_SUCCESS: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC process #%llu succeeded with KYC provider\n", + (unsigned long long) kpc->process_row); + qs = TEH_kyc_store_attributes ( + kpc->process_row, + &kpc->h_payto, + provider_name, + provider_user_id, + provider_legitimization_id, + expiration, + attributes); + if (0 >= qs) + { + GNUNET_break (0); + proof_finish (kpc, + TALER_EC_GENERIC_DB_STORE_FAILED, + "kyc_store_attributes"); + break; + } + + kpc->kat = TEH_kyc_run_measure_for_attributes ( + &rc->async_scope_id, + kpc->process_row, + &kpc->h_payto, + kpc->is_wallet, + &proof_finish, + kpc); + if (NULL == kpc->kat) + { + GNUNET_break_op (0); + proof_finish (kpc, + TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, + NULL); + } + break; + case TALER_KYCLOGIC_STATUS_FAILED: + case TALER_KYCLOGIC_STATUS_PROVIDER_FAILED: + case TALER_KYCLOGIC_STATUS_USER_ABORTED: + case TALER_KYCLOGIC_STATUS_ABORTED: + GNUNET_assert (NULL == kpc->kat); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC process %s/%s (Row #%llu) failed: %d\n", + provider_user_id, + provider_legitimization_id, + (unsigned long long) kpc->process_row, + status); + if (5 == http_status / 100) + { + char *msg; + + /* OAuth2 server had a problem, do NOT log this as a KYC failure */ + GNUNET_break (0); + GNUNET_asprintf (&msg, + "Failure by KYC provider (HTTP status %u)\n", + http_status); + proof_finish ( + kpc, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + msg); + GNUNET_free (msg); + break; + } + if (! TEH_kyc_failed ( + kpc->process_row, + &kpc->h_payto, + kpc->provider_name, + provider_user_id, + provider_legitimization_id, + TALER_KYCLOGIC_status2s (status), + TALER_EC_EXCHANGE_GENERIC_KYC_FAILED)) + { + GNUNET_break (0); + proof_finish ( + kpc, + TALER_EC_GENERIC_DB_STORE_FAILED, + "TEH_kyc_failed"); + break; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "KYC process #%llu failed with status %d\n", + (unsigned long long) kpc->process_row, + status); + proof_finish (kpc, + TALER_EC_NONE, + NULL); + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC status of %s/%s (Row #%llu) is %d\n", + provider_user_id, + provider_legitimization_id, + (unsigned long long) kpc->process_row, + (int) status); + break; + } + GNUNET_async_scope_restore (&old_scope); +} + + +/** + * Function called to clean up a context. + * + * @param rc request context + */ +static void +clean_kpc (struct TEH_RequestContext *rc) +{ + struct KycProofContext *kpc = rc->rh_ctx; + + if (NULL != kpc->ph) + { + kpc->logic->proof_cancel (kpc->ph); + kpc->ph = NULL; + } + if (NULL != kpc->kat) + { + TEH_kyc_run_measure_cancel (kpc->kat); + kpc->kat = NULL; + } + if (NULL != kpc->response) + { + MHD_destroy_response (kpc->response); + kpc->response = NULL; + } + if (NULL != kpc->proof_response) + { + MHD_destroy_response (kpc->proof_response); + kpc->proof_response = NULL; + } + GNUNET_free (kpc->provider_user_id); + GNUNET_free (kpc->provider_legitimization_id); + GNUNET_free (kpc); +} + + +MHD_RESULT +TEH_handler_kyc_proof ( + struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct KycProofContext *kpc = rc->rh_ctx; + const char *provider_name_or_logic = args[0]; + + if (NULL == kpc) + { + /* first time */ + if (NULL == provider_name_or_logic) + { + GNUNET_break_op (0); + return respond_html_ec ( + rc, + MHD_HTTP_NOT_FOUND, + "kyc-proof-endpoint-unknown", + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + "'/kyc-proof/$PROVIDER_NAME?state=$H_PAYTO' required"); + } + kpc = GNUNET_new (struct KycProofContext); + kpc->rc = rc; + rc->rh_ctx = kpc; + rc->rh_cleaner = &clean_kpc; + TALER_MHD_parse_request_arg_auto_t (rc->connection, + "state", + &kpc->h_payto); + if (GNUNET_OK != + TALER_KYCLOGIC_lookup_logic ( + provider_name_or_logic, + &kpc->logic, + &kpc->pd, + &kpc->provider_name)) + { + GNUNET_break_op (0); + return respond_html_ec ( + rc, + MHD_HTTP_NOT_FOUND, + "kyc-proof-target-unknown", + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + provider_name_or_logic); + } + if (NULL != kpc->provider_name) + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute expiration; + + if (0 != strcmp (provider_name_or_logic, + kpc->provider_name)) + { + GNUNET_break_op (0); + return respond_html_ec ( + rc, + MHD_HTTP_BAD_REQUEST, + "kyc-proof-bad-request", + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "PROVIDER_NAME"); + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Looking for KYC process at %s\n", + kpc->provider_name); + qs = TEH_plugin->lookup_kyc_process_by_account ( + TEH_plugin->cls, + kpc->provider_name, + &kpc->h_payto, + &kpc->process_row, + &expiration, + &kpc->provider_user_id, + &kpc->provider_legitimization_id, + &kpc->is_wallet); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return respond_html_ec ( + rc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "kyc-proof-internal-error", + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_kyc_process_by_account"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break_op (0); + return respond_html_ec ( + rc, + MHD_HTTP_NOT_FOUND, + "kyc-proof-target-unknown", + TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, + kpc->provider_name); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + if (GNUNET_TIME_absolute_is_future (expiration)) + { + /* KYC not required */ + return respond_html_ec ( + rc, + MHD_HTTP_OK, + "kyc-proof-already-done", + TALER_EC_NONE, + NULL); + } + } + kpc->ph = kpc->logic->proof ( + kpc->logic->cls, + kpc->pd, + rc->connection, + &kpc->h_payto, + kpc->process_row, + kpc->provider_user_id, + kpc->provider_legitimization_id, + &proof_cb, + kpc); + if (NULL == kpc->ph) + { + GNUNET_break (0); + return respond_html_ec ( + rc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "kyc-proof-internal-error", + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "could not start proof with KYC logic"); + } + + + kpc->suspended = true; + GNUNET_CONTAINER_DLL_insert (kpc_head, + kpc_tail, + kpc); + MHD_suspend_connection (rc->connection); + return MHD_YES; + } + + if (NULL == kpc->response) + { + GNUNET_break (0); + return respond_html_ec ( + rc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "kyc-proof-internal-error", + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "handler resumed without response"); + } + + /* return response from KYC logic */ + return MHD_queue_response (rc->connection, + kpc->response_code, + kpc->response); +} + + +/* end of taler-exchange-httpd_kyc-proof.c */ diff --git a/src/exchange/taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.h b/src/exchange/taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + Copyright (C) 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-kyc-proof-PROVIDER_NAME.h + * @brief Handle /kyc-proof requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_KYC_PROOF_PROVIDER_NAME_H +#define TALER_EXCHANGE_HTTPD_GET_KYC_PROOF_PROVIDER_NAME_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown kyc-proof subsystem. Resumes all suspended long-polling clients + * and cleans up data structures. + */ +void +TEH_kyc_proof_cleanup (void); + + +/** + * Handle a "/kyc-proof" request. + * + * @param rc request to handle + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_proof ( + struct TEH_RequestContext *rc, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-metrics.c b/src/exchange/taler-exchange-httpd_get-metrics.c @@ -0,0 +1,160 @@ +/* + This file is part of TALER + Copyright (C) 2015-2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-metrics.c + * @brief Handle /metrics requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_json_lib.h> +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-metrics.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include <jansson.h> + + +unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; + +unsigned long long TEH_METRICS_withdraw_num_coins; + +unsigned long long TEH_METRICS_num_conflict[TEH_MT_REQUEST_COUNT]; + +unsigned long long TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_COUNT]; + +unsigned long long TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_COUNT]; + +unsigned long long TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_COUNT]; + +unsigned long long TEH_METRICS_num_success[TEH_MT_SUCCESS_COUNT]; + + +MHD_RESULT +TEH_handler_metrics (struct TEH_RequestContext *rc, + const char *const args[]) +{ + char *reply; + struct MHD_Response *resp; + MHD_RESULT ret; + + (void) args; + GNUNET_asprintf (&reply, + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "taler_exchange_success_transactions{type=\"%s\"} %llu\n" + "# HELP taler_exchange_serialization_failures " + " number of database serialization errors by type\n" + "# TYPE taler_exchange_serialization_failures counter\n" + "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" + "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" + "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" + "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" + "# HELP taler_exchange_received_requests " + " number of received requests by type\n" + "# TYPE taler_exchange_received_requests counter\n" + "taler_exchange_received_requests{type=\"%s\"} %llu\n" + "taler_exchange_received_requests{type=\"%s\"} %llu\n" + "taler_exchange_received_requests{type=\"%s\"} %llu\n" + "taler_exchange_received_requests{type=\"%s\"} %llu\n" + "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" +#if NOT_YET_IMPLEMENTED + "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" + "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" +#endif + "# HELP taler_exchange_num_signatures " + " number of signatures created by cipher\n" + "# TYPE taler_exchange_num_signatures counter\n" + "taler_exchange_num_signatures{type=\"%s\"} %llu\n" + "taler_exchange_num_signatures{type=\"%s\"} %llu\n" + "taler_exchange_num_signatures{type=\"%s\"} %llu\n" + "# HELP taler_exchange_num_signature_verifications " + " number of signatures verified by cipher\n" + "# TYPE taler_exchange_num_signature_verifications counter\n" + "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" + "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" + "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" + "# HELP taler_exchange_num_keyexchanges " + " number of key exchanges done by cipher\n" + "# TYPE taler_exchange_num_keyexchanges counter\n" + "taler_exchange_num_keyexchanges{type=\"%s\"} %llu\n" + "# HELP taler_exchange_withdraw_num_coins " + " number of coins withdrawn in a withdraw request\n" + "# TYPE taler_exchange_withdraw_num_coins counter\n" + "taler_exchange_withdraw_num_coins{} %llu\n", + "deposit", + TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT], + "withdraw", + TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW], + "melt", + TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT], + "refresh-reveal", + TEH_METRICS_num_success[TEH_MT_SUCCESS_REFRESH_REVEAL], + "other", + TEH_METRICS_num_conflict[TEH_MT_REQUEST_OTHER], + "deposit", + TEH_METRICS_num_conflict[TEH_MT_REQUEST_DEPOSIT], + "withdraw", + TEH_METRICS_num_conflict[TEH_MT_REQUEST_WITHDRAW], + "melt", + TEH_METRICS_num_conflict[TEH_MT_REQUEST_MELT], + "other", + TEH_METRICS_num_requests[TEH_MT_REQUEST_OTHER], + "deposit", + TEH_METRICS_num_requests[TEH_MT_REQUEST_DEPOSIT], + "withdraw", + TEH_METRICS_num_requests[TEH_MT_REQUEST_WITHDRAW], + "melt", + TEH_METRICS_num_requests[TEH_MT_REQUEST_MELT], + "withdraw", + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW], +#if NOT_YET_IMPLEMENTED + "deposit", + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT], + "melt", + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_MELT], + "kyc-upload", + TEH_METRICS_num_requests[TEH_MT_REQUEST_KYC_UPLOAD], +#endif + "rsa", + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA], + "cs", + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_CS], + "eddsa", + TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_EDDSA], + "rsa", + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA], + "cs", + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS], + "eddsa", + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA], + "ecdh", + TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_ECDH], + TEH_METRICS_withdraw_num_coins); + resp = MHD_create_response_from_buffer (strlen (reply), + reply, + MHD_RESPMEM_MUST_FREE); + ret = MHD_queue_response (rc->connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; +} + + +/* end of taler-exchange-httpd_metrics.c */ diff --git a/src/exchange/taler-exchange-httpd_get-metrics.h b/src/exchange/taler-exchange-httpd_get-metrics.h @@ -0,0 +1,132 @@ +/* + This file is part of TALER + Copyright (C) 2014--2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-metrics.h + * @brief Handle /metrics requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_METRICS_H +#define TALER_EXCHANGE_HTTPD_GET_METRICS_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Request types for which we collect metrics. + */ +enum TEH_MetricTypeRequest +{ + TEH_MT_REQUEST_OTHER = 0, + TEH_MT_REQUEST_DEPOSIT = 1, + TEH_MT_REQUEST_WITHDRAW = 2, + TEH_MT_REQUEST_MELT = 3, + TEH_MT_REQUEST_PURSE_CREATE = 4, + TEH_MT_REQUEST_PURSE_MERGE = 5, + TEH_MT_REQUEST_RESERVE_PURSE = 6, + TEH_MT_REQUEST_PURSE_DEPOSIT = 7, + TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT = 8, + TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW = 9, + TEH_MT_REQUEST_IDEMPOTENT_MELT = 10, + TEH_MT_REQUEST_BATCH_DEPOSIT = 11, + TEH_MT_REQUEST_POLICY_FULFILLMENT = 12, + TEH_MT_REQUEST_KYC_UPLOAD = 13, + TEH_MT_REQUEST_COUNT = 14 /* MUST BE LAST! */ +}; + +/** + * Success types for which we collect metrics. + */ +enum TEH_MetricTypeSuccess +{ + TEH_MT_SUCCESS_DEPOSIT = 0, + TEH_MT_SUCCESS_WITHDRAW = 1, + TEH_MT_SUCCESS_MELT = 2, + TEH_MT_SUCCESS_REFRESH_REVEAL = 3, + TEH_MT_SUCCESS_WITHDRAW_REVEAL = 4, + TEH_MT_SUCCESS_COUNT = 5 /* MUST BE LAST! */ +}; + +/** + * Cipher types for which we collect signature metrics. + */ +enum TEH_MetricTypeSignature +{ + TEH_MT_SIGNATURE_RSA = 0, + TEH_MT_SIGNATURE_CS = 1, + TEH_MT_SIGNATURE_EDDSA = 2, + TEH_MT_SIGNATURE_COUNT = 3 +}; + +/** + * Cipher types for which we collect key exchange metrics. + */ +enum TEH_MetricTypeKeyX +{ + TEH_MT_KEYX_ECDH = 0, + TEH_MT_KEYX_COUNT = 1 +}; + +/** + * Number of requests handled of the respective type. + */ +extern unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; + +/** + * Number of successful requests handled of the respective type. + */ +extern unsigned long long TEH_METRICS_num_success[TEH_MT_SUCCESS_COUNT]; + +/** + * Number of coins withdrawn in a withdraw request + */ +extern unsigned long long TEH_METRICS_withdraw_num_coins; + +/** + * Number of serialization errors encountered when + * handling requests of the respective type. + */ +extern unsigned long long TEH_METRICS_num_conflict[TEH_MT_REQUEST_COUNT]; + +/** + * Number of signatures created by the respective cipher. + */ +extern unsigned long long TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_COUNT]; + +/** + * Number of signatures verified by the respective cipher. + */ +extern unsigned long long TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_COUNT]; + +/** + * Number of key exchanges done with the respective cipher. + */ +extern unsigned long long TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_COUNT]; + +/** + * Handle a "/metrics" request. + * + * @param rc request context + * @param args array of additional options (must be empty for this function) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_metrics (struct TEH_RequestContext *rc, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-purses-PURSE_PUB-merge.c b/src/exchange/taler-exchange-httpd_get-purses-PURSE_PUB-merge.c @@ -0,0 +1,445 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-purses-PURSE_PUB-merge.c + * @brief Handle GET /purses/$PID/$TARGET requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_mhd_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-purses-PURSE_PUB-merge.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Information about an ongoing /purses GET operation. + */ +struct GetContext +{ + /** + * Kept in a DLL. + */ + struct GetContext *next; + + /** + * Kept in a DLL. + */ + struct GetContext *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Subscription for the database event we are + * waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Subscription for refund event we are + * waiting for. + */ + struct GNUNET_DB_EventHandler *ehr; + + /** + * Public key of our purse. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * When does this purse expire? + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * When was this purse merged? + */ + struct GNUNET_TIME_Timestamp merge_timestamp; + + /** + * How much is the purse (supposed) to be worth? + */ + struct TALER_Amount amount; + + /** + * How much was deposited into the purse so far? + */ + struct TALER_Amount deposited; + + /** + * Hash over the contract of the purse. + */ + struct TALER_PrivateContractHashP h_contract; + + /** + * When will this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * true to wait for merge, false to wait for deposit. + */ + bool wait_for_merge; + + /** + * True if we are still suspended. + */ + bool suspended; +}; + + +/** + * Head of DLL of suspended GET requests. + */ +static struct GetContext *gc_head; + +/** + * Tail of DLL of suspended GET requests. + */ +static struct GetContext *gc_tail; + + +void +TEH_purses_get_cleanup () +{ + struct GetContext *gc; + + while (NULL != (gc = gc_head)) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + if (gc->suspended) + { + gc->suspended = false; + MHD_resume_connection (gc->connection); + } + } +} + + +/** + * Function called once a connection is done to + * clean up the `struct GetContext` state. + * + * @param rc context to clean up for + */ +static void +gc_cleanup (struct TEH_RequestContext *rc) +{ + struct GetContext *gc = rc->rh_ctx; + + GNUNET_assert (! gc->suspended); + if (NULL != gc->eh) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancelling DB event listening\n"); + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + gc->eh); + gc->eh = NULL; + } + if (NULL != gc->ehr) + { + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + gc->ehr); + gc->ehr = NULL; + } + GNUNET_free (gc); +} + + +/** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct TEH_RequestContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct TEH_RequestContext *rc = cls; + struct GetContext *gc = rc->rh_ctx; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + if (NULL == gc) + return; /* event triggered while main transaction + was still running */ + if (! gc->suspended) + return; /* might get multiple wake-up events */ + gc->suspended = false; + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Waking up on %p - %p - %s\n", + rc, + gc, + gc->suspended ? "suspended" : "active"); + TEH_check_invariants (); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + MHD_resume_connection (gc->connection); + TALER_MHD_daemon_trigger (); + TEH_check_invariants (); + GNUNET_async_scope_restore (&old_scope); +} + + +MHD_RESULT +TEH_handler_purses_get (struct TEH_RequestContext *rc, + const char *const args[2]) +{ + struct GetContext *gc = rc->rh_ctx; + bool purse_deleted; + bool purse_refunded; + MHD_RESULT res; + + if (NULL == gc) + { + gc = GNUNET_new (struct GetContext); + rc->rh_ctx = gc; + rc->rh_cleaner = &gc_cleanup; + gc->connection = rc->connection; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &gc->purse_pub, + sizeof (gc->purse_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, + args[0]); + } + if (0 == strcmp (args[1], + "merge")) + gc->wait_for_merge = true; + else if (0 == strcmp (args[1], + "deposit")) + gc->wait_for_merge = false; + else + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_PURSES_INVALID_WAIT_TARGET, + args[1]); + } + + TALER_MHD_parse_request_timeout (rc->connection, + &gc->timeout); + if ( (GNUNET_TIME_absolute_is_future (gc->timeout)) && + (NULL == gc->eh) ) + { + struct TALER_PurseEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons ( + gc->wait_for_merge + ? TALER_DBEVENT_EXCHANGE_PURSE_MERGED + : TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), + .purse_pub = gc->purse_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting DB event listening on purse %s (%s)\n", + TALER_B2S (&gc->purse_pub), + gc->wait_for_merge + ? "waiting for merge" + : "waiting for deposit"); + gc->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (gc->timeout), + &rep.header, + &db_event_cb, + rc); + if (NULL == gc->eh) + { + GNUNET_break (0); + gc->timeout = GNUNET_TIME_UNIT_ZERO_ABS; + } + else + { + struct GNUNET_DB_EventHeaderP repr = { + .size = htons (sizeof (repr)), + .type = htons (TALER_DBEVENT_EXCHANGE_PURSE_REFUNDED), + }; + + gc->ehr = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (gc->timeout), + &repr, + &db_event_cb, + rc); + } + } + } /* end first-time initialization */ + + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp create_timestamp; + + qs = TEH_plugin->select_purse (TEH_plugin->cls, + &gc->purse_pub, + &create_timestamp, + &gc->purse_expiration, + &gc->amount, + &gc->deposited, + &gc->h_contract, + &gc->merge_timestamp, + &purse_deleted, + &purse_refunded); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "select_purse %s returned %d (%s)\n", + args[0], + (int) qs, + GNUNET_TIME_timestamp2s (gc->merge_timestamp)); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_purse"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_purse"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* handled below */ + } + } + if (purse_refunded || + purse_deleted) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Purse refunded or deleted\n"); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_GONE, + purse_deleted + ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED + : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED + , + GNUNET_TIME_timestamp2s ( + gc->purse_expiration)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Deposited amount is %s (%d/%d/%d)\n", + TALER_amount2s (&gc->deposited), + GNUNET_TIME_absolute_is_future (gc->timeout), + GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time), + (0 < + TALER_amount_cmp (&gc->amount, + &gc->deposited))); + if (GNUNET_TIME_absolute_is_future (gc->timeout) && + ( ((gc->wait_for_merge) && + GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) || + ((! gc->wait_for_merge) && + (0 < + TALER_amount_cmp (&gc->amount, + &gc->deposited))) ) ) + { + gc->suspended = true; + GNUNET_CONTAINER_DLL_insert (gc_head, + gc_tail, + gc); + MHD_suspend_connection (gc->connection); + return MHD_YES; + } + + { + struct GNUNET_TIME_Timestamp dt = GNUNET_TIME_timestamp_get (); + struct TALER_ExchangePublicKeyP exchange_pub; + struct TALER_ExchangeSignatureP exchange_sig; + enum TALER_ErrorCode ec; + + if (GNUNET_TIME_timestamp_cmp (dt, + >, + gc->purse_expiration)) + dt = gc->purse_expiration; + if (0 < + TALER_amount_cmp (&gc->amount, + &gc->deposited)) + { + /* amount > deposited: not yet fully paid */ + dt = GNUNET_TIME_UNIT_ZERO_TS; + } + if (TALER_EC_NONE != + (ec = TALER_exchange_online_purse_status_sign ( + &TEH_keys_exchange_sign_, + gc->merge_timestamp, + dt, + &gc->deposited, + &exchange_pub, + &exchange_sig))) + { + res = TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); + } + else + { + /* Make sure merge_timestamp is omitted if not yet merged */ + if (GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) + gc->merge_timestamp = GNUNET_TIME_UNIT_ZERO_TS; + res = TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("balance", + &gc->deposited), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_pack_timestamp ("purse_expiration", + gc->purse_expiration), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("merge_timestamp", + gc->merge_timestamp)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("deposit_timestamp", + dt)) + ); + } + } + return res; +} + + +/* end of taler-exchange-httpd_purses_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-purses-PURSE_PUB-merge.h b/src/exchange/taler-exchange-httpd_get-purses-PURSE_PUB-merge.h @@ -0,0 +1,51 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-purses-PURSE_PUB-merge.h + * @brief Handle /purses/$PURSE_PUB/$TARGET GET requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_PURSES_PURSE_PUB_MERGE_H +#define TALER_EXCHANGE_HTTPD_GET_PURSES_PURSE_PUB_MERGE_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown purses-get subsystem. Resumes all + * suspended long-polling clients and cleans up + * data structures. + */ +void +TEH_purses_get_cleanup (void); + + +/** + * Handle a GET "/purses/$PID/$TARGET" request. Parses the + * given "purse_pub" in @a args (which should contain the + * EdDSA public key of a purse) and then respond with the + * status of the purse. + * + * @param rc request context + * @param args array of additional options (length: 2, the purse_pub and a target) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_get (struct TEH_RequestContext *rc, + const char *const args[2]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c b/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c @@ -0,0 +1,633 @@ +/* + This file is part of TALER + Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c + * @brief Handle /reserves/$RESERVE_PUB HISTORY requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-reserves-RESERVE_PUB-history.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Compile the history of a reserve into a JSON object. + * + * @param rh reserve history to JSON-ify + * @return json representation of the @a rh, NULL on error + */ +static json_t * +compile_reserve_history ( + const struct TALER_EXCHANGEDB_ReserveHistory *rh) +{ + json_t *json_history; + + json_history = json_array (); + GNUNET_assert (NULL != json_history); + for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh; + NULL != pos; + pos = pos->next) + { + switch (pos->type) + { + case TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE: + { + const struct TALER_EXCHANGEDB_BankTransfer *bank = + pos->details.bank; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "CREDIT"), + GNUNET_JSON_pack_timestamp ("timestamp", + bank->execution_date), + TALER_JSON_pack_full_payto ("sender_account_url", + bank->sender_account_details), + GNUNET_JSON_pack_uint64 ("wire_reference", + bank->wire_reference), + TALER_JSON_pack_amount ("amount", + &bank->amount)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + break; + } + case TALER_EXCHANGEDB_RO_WITHDRAW_COINS: + { + const struct TALER_EXCHANGEDB_Withdraw *withdraw + = pos->details.withdraw; + struct TALER_Amount withdraw_fee; + struct TEH_KeyStateHandle *ksh; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + TEH_currency, + &withdraw_fee)); + + /* + * We need to calculate the fees for the withdraw. + * Therefore, we need to get access to the key state. + */ + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + + for (size_t i = 0; i < withdraw->num_coins; i++) + { + /* Find the denomination and accumulate the fee */ + { + struct TEH_DenominationKey *dk; + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + &withdraw->denom_pub_hashes[i], + NULL, + NULL); + + if (NULL == dk) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + + if (0 > TALER_amount_add (&withdraw_fee, + &withdraw_fee, + &dk->meta.fees.withdraw)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + } + + /* Prepare the entry for the history */ + { + json_t *j_entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ( + "type", + "WITHDRAW"), + GNUNET_JSON_pack_data_auto ( + "reserve_sig", + &withdraw->reserve_sig), + GNUNET_JSON_pack_data_auto ( + "planchets_h", + &withdraw->planchets_h), + GNUNET_JSON_pack_uint64 ( + "num_coins", + withdraw->num_coins), + TALER_JSON_pack_array_of_data ( + "denom_pub_hashes", + withdraw->num_coins, + withdraw->denom_pub_hashes, + sizeof(withdraw->denom_pub_hashes[0])), + TALER_JSON_pack_amount ( + "withdraw_fee", + &withdraw_fee), + TALER_JSON_pack_amount ( + "amount", + &withdraw->amount_with_fee) + ); + + if (! withdraw->no_blinding_seed) + { + if (0!= + json_object_update_new ( + j_entry, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ( + "blinding_seed", + &withdraw->blinding_seed)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + if (0!= + json_object_update_new ( + j_entry, + GNUNET_JSON_PACK ( + TALER_JSON_pack_array_of_data ( + "cs_r_values", + withdraw->num_cs_r_values, + withdraw->cs_r_values, + sizeof(withdraw->cs_r_values[0]))))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + if (withdraw->age_proof_required) + { + if (0 != + json_object_update_new ( + j_entry, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ( + "noreveal_index", + withdraw->noreveal_index), + GNUNET_JSON_pack_data_auto ( + "selected_h", + &withdraw->selected_h), + GNUNET_JSON_pack_uint64 ( + "max_age", + withdraw->max_age)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + + if (0 != + json_array_append_new ( + json_history, + j_entry)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + } + + break; + case TALER_EXCHANGEDB_RO_RECOUP_COIN: + { + const struct TALER_EXCHANGEDB_Recoup *recoup + = pos->details.recoup; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + if (TALER_EC_NONE != + TALER_exchange_online_confirm_recoup_sign ( + &TEH_keys_exchange_sign_, + recoup->timestamp, + &recoup->value, + &recoup->coin.coin_pub, + &recoup->reserve_pub, + &pub, + &sig)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "RECOUP"), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_timestamp ("timestamp", + recoup->timestamp), + TALER_JSON_pack_amount ("amount", + &recoup->value), + GNUNET_JSON_pack_data_auto ("coin_pub", + &recoup->coin.coin_pub)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_RO_EXCHANGE_TO_BANK: + { + const struct TALER_EXCHANGEDB_ClosingTransfer *closing = + pos->details.closing; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + if (TALER_EC_NONE != + TALER_exchange_online_reserve_closed_sign ( + &TEH_keys_exchange_sign_, + closing->execution_date, + &closing->amount, + &closing->closing_fee, + closing->receiver_account_details, + &closing->wtid, + &pos->details.closing->reserve_pub, + &pub, + &sig)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "CLOSING"), + TALER_JSON_pack_full_payto ("receiver_account_details", + closing->receiver_account_details), + GNUNET_JSON_pack_data_auto ("wtid", + &closing->wtid), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_timestamp ("timestamp", + closing->execution_date), + TALER_JSON_pack_amount ("amount", + &closing->amount), + TALER_JSON_pack_amount ("closing_fee", + &closing->closing_fee)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_RO_PURSE_MERGE: + { + const struct TALER_EXCHANGEDB_PurseMerge *merge = + pos->details.merge; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "MERGE"), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &merge->h_contract_terms), + GNUNET_JSON_pack_data_auto ("merge_pub", + &merge->merge_pub), + GNUNET_JSON_pack_uint64 ("min_age", + merge->min_age), + GNUNET_JSON_pack_uint64 ("flags", + merge->flags), + GNUNET_JSON_pack_data_auto ("purse_pub", + &merge->purse_pub), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &merge->reserve_sig), + GNUNET_JSON_pack_timestamp ("merge_timestamp", + merge->merge_timestamp), + GNUNET_JSON_pack_timestamp ("purse_expiration", + merge->purse_expiration), + TALER_JSON_pack_amount ("purse_fee", + &merge->purse_fee), + TALER_JSON_pack_amount ("amount", + &merge->amount_with_fee), + GNUNET_JSON_pack_bool ("merged", + merge->merged)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + case TALER_EXCHANGEDB_RO_HISTORY_REQUEST: + { + const struct TALER_EXCHANGEDB_HistoryRequest *history = + pos->details.history; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "HISTORY"), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &history->reserve_sig), + GNUNET_JSON_pack_timestamp ("request_timestamp", + history->request_timestamp), + TALER_JSON_pack_amount ("amount", + &history->history_fee)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + + case TALER_EXCHANGEDB_RO_OPEN_REQUEST: + { + const struct TALER_EXCHANGEDB_OpenRequest *orq = + pos->details.open_request; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "OPEN"), + GNUNET_JSON_pack_uint64 ("requested_min_purses", + orq->purse_limit), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &orq->reserve_sig), + GNUNET_JSON_pack_timestamp ("request_timestamp", + orq->request_timestamp), + GNUNET_JSON_pack_timestamp ("requested_expiration", + orq->reserve_expiration), + TALER_JSON_pack_amount ("open_fee", + &orq->open_fee)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + + case TALER_EXCHANGEDB_RO_CLOSE_REQUEST: + { + const struct TALER_EXCHANGEDB_CloseRequest *crq = + pos->details.close_request; + + if (0 != + json_array_append_new ( + json_history, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "CLOSE"), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &crq->reserve_sig), + GNUNET_is_zero (&crq->target_account_h_payto) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("h_payto", + NULL)) + : GNUNET_JSON_pack_data_auto ("h_payto", + &crq->target_account_h_payto), + GNUNET_JSON_pack_timestamp ("request_timestamp", + crq->request_timestamp)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + break; + } + } + return json_history; +} + + +/** + * Add the headers we want to set for every /keys response. + * + * @param cls the key state to use + * @param[in,out] response the response to modify + */ +static void +add_response_headers (void *cls, + struct MHD_Response *response) +{ + (void) cls; + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-cache")); +} + + +MHD_RESULT +TEH_handler_reserves_history ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub) +{ + struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; + uint64_t start_off = 0; + struct TALER_Amount balance; + uint64_t etag_in; + uint64_t etag_out; + char etagp[24]; + struct MHD_Response *resp; + unsigned int http_status; + + TALER_MHD_parse_request_number (rc->connection, + "start", + &start_off); + { + struct TALER_ReserveSignatureP reserve_sig; + bool required = true; + + TALER_MHD_parse_request_header_auto (rc->connection, + TALER_RESERVE_HISTORY_SIGNATURE_HEADER, + &reserve_sig, + required); + + if (GNUNET_OK != + TALER_wallet_reserve_history_verify (start_off, + reserve_pub, + &reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE, + NULL); + } + } + + /* Get etag */ + { + const char *etags; + + etags = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != etags) + { + char dummy; + unsigned long long ev; + + if (1 != sscanf (etags, + "\"%llu\"%c", + &ev, + &dummy)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client send malformed `If-None-Match' header `%s'\n", + etags); + etag_in = 0; + } + else + { + etag_in = (uint64_t) ev; + } + } + else + { + etag_in = start_off; + } + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_reserve_history (TEH_plugin->cls, + reserve_pub, + start_off, + etag_in, + &etag_out, + &balance, + &rh); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_reserve_history"); + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_reserve_history"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Handled below */ + break; + } + } + + GNUNET_snprintf (etagp, + sizeof (etagp), + "\"%llu\"", + (unsigned long long) etag_out); + if (etag_in == etag_out) + { + TEH_plugin->free_reserve_history (TEH_plugin->cls, + rh); + return TEH_RESPONSE_reply_not_modified (rc->connection, + etagp, + &add_response_headers, + NULL); + } + if (NULL == rh) + { + /* 204: empty history */ + resp = MHD_create_response_from_buffer_static (0, + ""); + http_status = MHD_HTTP_NO_CONTENT; + } + else + { + json_t *history; + + http_status = MHD_HTTP_OK; + history = compile_reserve_history (rh); + TEH_plugin->free_reserve_history (TEH_plugin->cls, + rh); + rh = NULL; + if (NULL == history) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + NULL); + } + resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_amount ("balance", + &balance), + GNUNET_JSON_pack_array_steal ("history", + history)); + } + add_response_headers (NULL, + resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etagp)); + { + MHD_RESULT ret; + + ret = MHD_queue_response (rc->connection, + http_status, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } +} + + +/* end of taler-exchange-httpd_reserves_history.c */ diff --git a/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.h b/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-reserves-RESERVE_PUB-history.h + * @brief Handle /reserves/$RESERVE_PUB/history requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_RESERVES_RESERVE_PUB_HISTORY_H +#define TALER_EXCHANGE_HTTPD_GET_RESERVES_RESERVE_PUB_HISTORY_H + +#include <microhttpd.h> +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/reserves/$RID/history" request. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_history ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB.c b/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB.c @@ -0,0 +1,278 @@ +/* + This file is part of TALER + Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-reserves-RESERVE_PUB.c + * @brief Handle /reserves/$RESERVE_PUB GET requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_mhd_lib.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-reserves-RESERVE_PUB.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Reserve GET request that is long-polling. + */ +struct ReservePoller +{ + /** + * Kept in a DLL. + */ + struct ReservePoller *next; + + /** + * Kept in a DLL. + */ + struct ReservePoller *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Our request context. + */ + struct TEH_RequestContext *rc; + + /** + * Subscription for the database event we are waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * When will this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Public key of the reserve the inquiry is about. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Balance of the reserve, set in the callback. + */ + struct TALER_Amount balance; + + /** + * Last origin account of the reserve, NULL if only + * P2P payments were made. + */ + struct TALER_FullPayto origin_account; + + /** + * True if we are still suspended. + */ + bool suspended; + +}; + + +/** + * Head of list of requests in long polling. + */ +static struct ReservePoller *rp_head; + +/** + * Tail of list of requests in long polling. + */ +static struct ReservePoller *rp_tail; + + +void +TEH_reserves_get_cleanup () +{ + for (struct ReservePoller *rp = rp_head; + NULL != rp; + rp = rp->next) + { + if (rp->suspended) + { + rp->suspended = false; + MHD_resume_connection (rp->connection); + } + } +} + + +/** + * Function called once a connection is done to + * clean up the `struct ReservePoller` state. + * + * @param rc context to clean up for + */ +static void +rp_cleanup (struct TEH_RequestContext *rc) +{ + struct ReservePoller *rp = rc->rh_ctx; + + GNUNET_assert (! rp->suspended); + if (NULL != rp->eh) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancelling DB event listening on cleanup (odd unless during shutdown)\n"); + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + rp->eh); + rp->eh = NULL; + } + GNUNET_CONTAINER_DLL_remove (rp_head, + rp_tail, + rp); + GNUNET_free (rp->origin_account.full_payto); + GNUNET_free (rp); +} + + +/** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct TEH_RequestContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct ReservePoller *rp = cls; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + if (! rp->suspended) + return; /* might get multiple wake-up events */ + GNUNET_async_scope_enter (&rp->rc->async_scope_id, + &old_scope); + TEH_check_invariants (); + rp->suspended = false; + MHD_resume_connection (rp->connection); + TALER_MHD_daemon_trigger (); + TEH_check_invariants (); + GNUNET_async_scope_restore (&old_scope); +} + + +MHD_RESULT +TEH_handler_reserves_get ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub) +{ + struct ReservePoller *rp = rc->rh_ctx; + + if (NULL == rp) + { + rp = GNUNET_new (struct ReservePoller); + rp->connection = rc->connection; + rp->rc = rc; + rc->rh_ctx = rp; + rc->rh_cleaner = &rp_cleanup; + GNUNET_CONTAINER_DLL_insert (rp_head, + rp_tail, + rp); + rp->reserve_pub = *reserve_pub; + TALER_MHD_parse_request_timeout (rc->connection, + &rp->timeout); + } + + if ( (GNUNET_TIME_absolute_is_future (rp->timeout)) && + (NULL == rp->eh) ) + { + struct TALER_ReserveEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_RESERVE_INCOMING), + .reserve_pub = rp->reserve_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting DB event listening until %s\n", + GNUNET_TIME_absolute2s (rp->timeout)); + rp->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (rp->timeout), + &rep.header, + &db_event_cb, + rp); + } + { + enum GNUNET_DB_QueryStatus qs; + + GNUNET_free (rp->origin_account.full_payto); + qs = TEH_plugin->get_reserve_balance (TEH_plugin->cls, + &rp->reserve_pub, + &rp->balance, + &rp->origin_account); + switch (qs) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); /* single-shot query should never have soft-errors */ + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_reserve_balance"); + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_reserve_balance"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got reserve balance of %s\n", + TALER_amount2s (&rp->balance)); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "last_origin", + rp->origin_account.full_payto)), + TALER_JSON_pack_amount ("balance", + &rp->balance)); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (! GNUNET_TIME_absolute_is_future (rp->timeout)) + { + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Long-polling on reserve for %s\n", + GNUNET_STRINGS_relative_time_to_string ( + GNUNET_TIME_absolute_get_remaining (rp->timeout), + true)); + rp->suspended = true; + MHD_suspend_connection (rc->connection); + return MHD_YES; + } + } + GNUNET_break (0); + return MHD_NO; +} + + +/* end of taler-exchange-httpd_reserves_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB.h b/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB.h @@ -0,0 +1,54 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-reserves-RESERVE_PUB.h + * @brief Handle /reserves/$RESERVE_PUB GET requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_RESERVES_RESERVE_PUB_H +#define TALER_EXCHANGE_HTTPD_GET_RESERVES_RESERVE_PUB_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Shutdown reserves-get subsystem. Resumes all + * suspended long-polling clients and cleans up + * data structures. + */ +void +TEH_reserves_get_cleanup (void); + + +/** + * Handle a GET "/reserves/" request. Parses the + * given "reserve_pub" in @a args (which should contain the + * EdDSA public key of a reserve) and then respond with the + * status of the reserve. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_get ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.c b/src/exchange/taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.c @@ -0,0 +1,195 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.c + * @brief Handle GET /reserves/$RESERVE_PUB/attest requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for #reserve_attest_transaction. + */ +struct ReserveAttestContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Hash of the payto URI of this reserve. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * Available attributes. + */ + json_t *attributes; + +}; + + +/** + * Function called with information about all applicable + * legitimization processes for the given user. + * + * @param cls our `struct ReserveAttestContext *` + * @param h_payto account for which the attribute data is stored + * @param provider_name provider that must be checked + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + */ +static void +kyc_process_cb (void *cls, + const struct TALER_NormalizedPaytoHashP *h_payto, + const char *provider_name, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes) +{ + struct ReserveAttestContext *rsc = cls; + json_t *attrs; + json_t *val; + const char *name; + + if (GNUNET_TIME_absolute_is_past ( + expiration_time.abs_time)) + return; + attrs = TALER_CRYPTO_kyc_attributes_decrypt ( + &TEH_attribute_key, + enc_attributes, + enc_attributes_size); + json_object_foreach (attrs, name, val) + { + bool duplicate = false; + size_t idx; + json_t *str; + + json_array_foreach (rsc->attributes, idx, str) + { + if (0 == strcmp (json_string_value (str), + name)) + { + duplicate = true; + break; + } + } + if (duplicate) + continue; + GNUNET_assert (0 == + json_array_append_new (rsc->attributes, + json_string (name))); + } + json_decref (attrs); +} + + +MHD_RESULT +TEH_handler_reserves_get_attest ( + struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct ReserveAttestContext rsc = { + .attributes = NULL + }; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + args[0], + strlen (args[0]), + &rsc.reserve_pub, + sizeof (rsc.reserve_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, + args[0]); + } + { + struct TALER_NormalizedPayto payto_uri; + + payto_uri + = TALER_reserve_make_payto (TEH_base_url, + &rsc.reserve_pub); + TALER_normalized_payto_hash (payto_uri, + &rsc.h_payto); + GNUNET_free (payto_uri.normalized_payto); + } + { + enum GNUNET_DB_QueryStatus qs; + + rsc.attributes = json_array (); + GNUNET_assert (NULL != rsc.attributes); + qs = TEH_plugin->select_kyc_attributes (TEH_plugin->cls, + &rsc.h_payto, + &kyc_process_cb, + &rsc); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + json_decref (rsc.attributes); + rsc.attributes = NULL; + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_kyc_attributes"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + json_decref (rsc.attributes); + rsc.attributes = NULL; + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_kyc_attributes"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break_op (0); + json_decref (rsc.attributes); + rsc.attributes = NULL; + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("details", + rsc.attributes)); +} + + +/* end of taler-exchange-httpd_reserves_get_attest.c */ diff --git a/src/exchange/taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.h b/src/exchange/taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-reserves-attest-RESERVE_PUB.h + * @brief Handle /reserves/$RESERVE_PUB GET_ATTEST requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_RESERVES_ATTEST_RESERVE_PUB_H +#define TALER_EXCHANGE_HTTPD_GET_RESERVES_ATTEST_RESERVE_PUB_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/reserves/$RID/attest" request. Parses the + * given "reserve_pub" in @a args (which should contain the + * EdDSA public key of a reserve) and then responds with the + * available attestations for the reserve. + * + * @param rc request context + * @param args array of additional options (length: 1, just the reserve_pub) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_get_attest (struct TEH_RequestContext *rc, + const char *const args[1]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-transfers-WTID.c b/src/exchange/taler-exchange-httpd_get-transfers-WTID.c @@ -0,0 +1,641 @@ +/* + This file is part of TALER + Copyright (C) 2014-2018, 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-transfers-WTID.c + * @brief Handle wire transfer(s) GET requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-transfers-WTID.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" + + +/** + * Information about one of the transactions that was + * aggregated, to be returned in the /transfers response. + */ +struct AggregatedDepositDetail +{ + + /** + * We keep deposit details in a DLL. + */ + struct AggregatedDepositDetail *next; + + /** + * We keep deposit details in a DLL. + */ + struct AggregatedDepositDetail *prev; + + /** + * Hash of the contract terms. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Coin's public key of the deposited coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Total value of the coin in the deposit (after + * refunds). + */ + struct TALER_Amount deposit_value; + + /** + * Fees charged by the exchange for the deposit of this coin (possibly after reduction due to refunds). + */ + struct TALER_Amount deposit_fee; + + /** + * Total amount refunded for this coin. + */ + struct TALER_Amount refund_total; +}; + + +/** + * A merchant asked for transaction details about a wire transfer. + * Provide them. Generates the 200 reply. + * + * @param connection connection to the client + * @param total total amount that was transferred + * @param merchant_pub public key of the merchant + * @param payto_uri destination account + * @param wire_fee wire fee that was charged + * @param exec_time execution time of the wire transfer + * @param wdd_head linked list with details about the combined deposits + * @return MHD result code + */ +static MHD_RESULT +reply_transfer_details (struct MHD_Connection *connection, + const struct TALER_Amount *total, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_FullPayto payto_uri, + const struct TALER_Amount *wire_fee, + struct GNUNET_TIME_Timestamp exec_time, + const struct AggregatedDepositDetail *wdd_head) +{ + json_t *deposits; + struct GNUNET_HashContext *hash_context; + struct GNUNET_HashCode h_details; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + struct TALER_FullPaytoHashP h_payto; + + deposits = json_array (); + GNUNET_assert (NULL != deposits); + hash_context = GNUNET_CRYPTO_hash_context_start (); + for (const struct AggregatedDepositDetail *wdd_pos = wdd_head; + NULL != wdd_pos; + wdd_pos = wdd_pos->next) + { + TALER_exchange_online_wire_deposit_append (hash_context, + &wdd_pos->h_contract_terms, + exec_time, + &wdd_pos->coin_pub, + &wdd_pos->deposit_value, + &wdd_pos->deposit_fee); + if (0 != + json_array_append_new ( + deposits, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &wdd_pos->h_contract_terms), + GNUNET_JSON_pack_data_auto ("coin_pub", + &wdd_pos->coin_pub), + + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("refund_total", + TALER_amount_is_zero ( + &wdd_pos->refund_total) + ? NULL + : &wdd_pos->refund_total)), + TALER_JSON_pack_amount ("deposit_value", + &wdd_pos->deposit_value), + TALER_JSON_pack_amount ("deposit_fee", + &wdd_pos->deposit_fee)))) + { + json_decref (deposits); + GNUNET_CRYPTO_hash_context_abort (hash_context); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + "json_array_append_new() failed"); + } + } + GNUNET_CRYPTO_hash_context_finish (hash_context, + &h_details); + { + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_wire_deposit_sign ( + &TEH_keys_exchange_sign_, + total, + wire_fee, + merchant_pub, + payto_uri, + &h_details, + &pub, + &sig))) + { + json_decref (deposits); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + } + + TALER_full_payto_hash (payto_uri, + &h_payto); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("total", + total), + TALER_JSON_pack_amount ("wire_fee", + wire_fee), + GNUNET_JSON_pack_data_auto ("merchant_pub", + merchant_pub), + GNUNET_JSON_pack_data_auto ("h_payto", + &h_payto), + GNUNET_JSON_pack_timestamp ("execution_time", + exec_time), + GNUNET_JSON_pack_array_steal ("deposits", + deposits), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * Closure for #handle_transaction_data. + */ +struct WtidTransactionContext +{ + + /** + * Identifier of the wire transfer to track. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Total amount of the wire transfer, as calculated by + * summing up the individual amounts. To be rounded down + * to calculate the real transfer amount at the end. + * Only valid if @e is_valid is #GNUNET_YES. + */ + struct TALER_Amount total; + + /** + * Public key of the merchant, only valid if @e is_valid + * is #GNUNET_YES. + */ + struct TALER_MerchantPublicKeyP merchant_pub; + + /** + * Wire fees applicable at @e exec_time. + */ + struct TALER_WireFeeSet fees; + + /** + * Execution time of the wire transfer + */ + struct GNUNET_TIME_Timestamp exec_time; + + /** + * Head of DLL with deposit details for transfers GET response. + */ + struct AggregatedDepositDetail *wdd_head; + + /** + * Tail of DLL with deposit details for transfers GET response. + */ + struct AggregatedDepositDetail *wdd_tail; + + /** + * Where were the funds wired? + */ + struct TALER_FullPayto payto_uri; + + /** + * JSON array with details about the individual deposits. + */ + json_t *deposits; + + /** + * Initially #GNUNET_NO, if we found no deposits so far. Set to + * #GNUNET_YES if we got transaction data, and the database replies + * remained consistent with respect to @e merchant_pub and @e h_wire + * (as they should). Set to #GNUNET_SYSERR if we encountered an + * internal error. + */ + enum GNUNET_GenericReturnValue is_valid; + +}; + + +/** + * Callback that totals up the applicable refunds. + * + * @param cls a `struct TALER_Amount` where we keep the total + * @param amount_with_fee amount being refunded + */ +static enum GNUNET_GenericReturnValue +add_refunds (void *cls, + const struct TALER_Amount *amount_with_fee) + +{ + struct TALER_Amount *total = cls; + + if (0 > + TALER_amount_add (total, + total, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Function called with the results of the lookup of the individual deposits + * that were aggregated for the given wire transfer. + * + * @param cls our context for transmission + * @param rowid which row in the DB is the information from (for diagnostics), ignored + * @param merchant_pub public key of the merchant (should be same for all callbacks with the same @e cls) + * @param account_payto_uri where the funds were sent + * @param h_payto hash over @a account_payto_uri as it is in the DB + * @param exec_time execution time of the wire transfer (should be same for all callbacks with the same @e cls) + * @param h_contract_terms which proposal was this payment about + * @param denom_pub denomination public key of the @a coin_pub (ignored) + * @param coin_pub which public key was this payment about + * @param deposit_value amount contributed by this coin in total (including fee) + * @param deposit_fee deposit fee charged by exchange for this coin + */ +static void +handle_deposit_data ( + void *cls, + uint64_t rowid, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_FullPayto account_payto_uri, + const struct TALER_FullPaytoHashP *h_payto, + struct GNUNET_TIME_Timestamp exec_time, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *deposit_value, + const struct TALER_Amount *deposit_fee) +{ + struct WtidTransactionContext *ctx = cls; + struct TALER_Amount total_refunds; + struct TALER_Amount dval; + struct TALER_Amount dfee; + enum GNUNET_DB_QueryStatus qs; + + (void) rowid; + (void) denom_pub; + (void) h_payto; + if (GNUNET_SYSERR == ctx->is_valid) + return; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (deposit_value->currency, + &total_refunds)); + qs = TEH_plugin->select_refunds_by_coin (TEH_plugin->cls, + coin_pub, + merchant_pub, + h_contract_terms, + &add_refunds, + &total_refunds); + if (qs < 0) + { + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + if (1 == + TALER_amount_cmp (&total_refunds, + deposit_value)) + { + /* Refunds exceeded total deposit? not OK! */ + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + if (0 == + TALER_amount_cmp (&total_refunds, + deposit_value)) + { + /* total_refunds == deposit_value; + in this case, the total contributed to the + wire transfer is zero (as are fees) */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (deposit_value->currency, + &dval)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (deposit_value->currency, + &dfee)); + + } + else + { + /* Compute deposit value by subtracting refunds */ + GNUNET_assert (0 < + TALER_amount_subtract (&dval, + deposit_value, + &total_refunds)); + if (-1 == + TALER_amount_cmp (&dval, + deposit_fee)) + { + /* dval < deposit_fee, so after refunds less than + the deposit fee remains; reduce deposit fee to + the remaining value of the coin */ + dfee = dval; + } + else + { + /* Partial refund, deposit fee remains */ + dfee = *deposit_fee; + } + } + + if (GNUNET_NO == ctx->is_valid) + { + /* First one we encounter, setup general information in 'ctx' */ + ctx->merchant_pub = *merchant_pub; + ctx->payto_uri.full_payto = GNUNET_strdup (account_payto_uri.full_payto); + ctx->exec_time = exec_time; + ctx->is_valid = GNUNET_YES; + if (0 > + TALER_amount_subtract (&ctx->total, + &dval, + &dfee)) + { + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + } + else + { + struct TALER_Amount delta; + + /* Subsequent data, check general information matches that in 'ctx'; + (it should, otherwise the deposits should not have been aggregated) */ + if ( (0 != GNUNET_memcmp (&ctx->merchant_pub, + merchant_pub)) || + (0 != TALER_full_payto_cmp (account_payto_uri, + ctx->payto_uri)) ) + { + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + if (0 > + TALER_amount_subtract (&delta, + &dval, + &dfee)) + { + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + if (0 > + TALER_amount_add (&ctx->total, + &ctx->total, + &delta)) + { + GNUNET_break (0); + ctx->is_valid = GNUNET_SYSERR; + return; + } + } + + { + struct AggregatedDepositDetail *wdd; + + wdd = GNUNET_new (struct AggregatedDepositDetail); + wdd->deposit_value = dval; + wdd->deposit_fee = dfee; + wdd->refund_total = total_refunds; + wdd->h_contract_terms = *h_contract_terms; + wdd->coin_pub = *coin_pub; + GNUNET_CONTAINER_DLL_insert (ctx->wdd_head, + ctx->wdd_tail, + wdd); + } +} + + +/** + * Free data structure reachable from @a ctx, but not @a ctx itself. + * + * @param ctx context to free + */ +static void +free_ctx (struct WtidTransactionContext *ctx) +{ + struct AggregatedDepositDetail *wdd; + + while (NULL != (wdd = ctx->wdd_head)) + { + GNUNET_CONTAINER_DLL_remove (ctx->wdd_head, + ctx->wdd_tail, + wdd); + GNUNET_free (wdd); + } + GNUNET_free (ctx->payto_uri.full_payto); +} + + +/** + * Execute a "/transfers" GET operation. Returns the deposit details of the + * deposits that were aggregated to create the given wire transfer. + * + * 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 closure + * @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 +get_transfer_deposits (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct WtidTransactionContext *ctx = cls; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp wire_fee_start_date; + struct GNUNET_TIME_Timestamp wire_fee_end_date; + struct TALER_MasterSignatureP wire_fee_master_sig; + + /* resetting to NULL/0 in case transaction was repeated after + serialization failure */ + free_ctx (ctx); + qs = TEH_plugin->lookup_wire_transfer (TEH_plugin->cls, + &ctx->wtid, + &handle_deposit_data, + ctx); + if (0 > qs) + { + 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, + "wire transfer"); + } + return qs; + } + if (GNUNET_SYSERR == ctx->is_valid) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "wire history malformed"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (GNUNET_NO == ctx->is_valid) + { + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + { + char *wire_method; + uint64_t rowid; + + wire_method = TALER_payto_get_method (ctx->payto_uri.full_payto); + if (NULL == wire_method) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "payto:// without wire method encountered"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + qs = TEH_plugin->get_wire_fee (TEH_plugin->cls, + wire_method, + ctx->exec_time, + &rowid, + &wire_fee_start_date, + &wire_fee_end_date, + &ctx->fees, + &wire_fee_master_sig); + GNUNET_free (wire_method); + } + if (0 >= qs) + { + if ( (GNUNET_DB_STATUS_HARD_ERROR == qs) || + (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND, + NULL); + } + return qs; + } + if (0 > + TALER_amount_subtract (&ctx->total, + &ctx->total, + &ctx->fees.wire)) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +MHD_RESULT +TEH_handler_transfers_get (struct TEH_RequestContext *rc, + const char *const args[1]) +{ + struct WtidTransactionContext ctx; + MHD_RESULT mhd_ret; + + memset (&ctx, + 0, + sizeof (ctx)); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &ctx.wtid, + sizeof (ctx.wtid))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_TRANSFERS_GET_WTID_MALFORMED, + args[0]); + } + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "run transfers GET", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &get_transfer_deposits, + &ctx)) + { + free_ctx (&ctx); + return mhd_ret; + } + mhd_ret = reply_transfer_details (rc->connection, + &ctx.total, + &ctx.merchant_pub, + ctx.payto_uri, + &ctx.fees.wire, + ctx.exec_time, + ctx.wdd_head); + free_ctx (&ctx); + return mhd_ret; +} + + +/* end of taler-exchange-httpd_transfers_get.c */ diff --git a/src/exchange/taler-exchange-httpd_get-transfers-WTID.h b/src/exchange/taler-exchange-httpd_get-transfers-WTID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + Copyright (C) 2014-2017, 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_get-transfers-WTID.h + * @brief Handle wire transfer tracking-related requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_GET_TRANSFERS_WTID_H +#define TALER_EXCHANGE_HTTPD_GET_TRANSFERS_WTID_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a GET "/transfers/$WTID" request. + * + * @param rc request context of the handler + * @param args array of additional options (length: 1, just the wtid) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_transfers_get (struct TEH_RequestContext *rc, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c @@ -1,4428 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020-2025 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 <http://www.gnu.org/licenses/> - */ -/** - * @file taler-exchange-httpd_keys.c - * @brief management of our various keys - * @author Christian Grothoff - * @author Özgür Kesim - */ -#include "taler/platform.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd.h" -#include "taler-exchange-httpd_config.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler/taler_extensions.h" - - -/** - * How many /keys request do we hold in suspension at - * most at any time? - */ -#define SKR_LIMIT 32 - - -/** - * When do we forcefully timeout a /keys request? - * Matches the 120s hard-coded into exchange_api_handle.c - */ -#define KEYS_TIMEOUT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 2) - - -/** - * Information about a denomination on offer by the denomination helper. - */ -struct HelperDenomination -{ - - /** - * When will the helper start to use this key for signing? - */ - struct GNUNET_TIME_Timestamp start_time; - - /** - * For how long will the helper allow signing? 0 if - * the key was revoked or purged. - */ - struct GNUNET_TIME_Relative validity_duration; - - /** - * Hash of the full denomination key. - */ - struct TALER_DenominationHashP h_denom_pub; - - /** - * Signature over this key from the security module's key. - */ - struct TALER_SecurityModuleSignatureP sm_sig; - - /** - * The (full) public key. - */ - struct TALER_DenominationPublicKey denom_pub; - - /** - * Details depend on the @e denom_pub.cipher type. - */ - union - { - - /** - * Hash of the RSA key. - */ - struct TALER_RsaPubHashP h_rsa; - - /** - * Hash of the CS key. - */ - struct TALER_CsPubHashP h_cs; - - } h_details; - - /** - * Name in configuration section for this denomination type. - */ - char *section_name; - - -}; - - -/** - * Signatures of an auditor over a denomination key of this exchange. - */ -struct TEH_AuditorSignature -{ - /** - * We store the signatures in a DLL. - */ - struct TEH_AuditorSignature *prev; - - /** - * We store the signatures in a DLL. - */ - struct TEH_AuditorSignature *next; - - /** - * A signature from the auditor. - */ - struct TALER_AuditorSignatureP asig; - - /** - * Public key of the auditor. - */ - struct TALER_AuditorPublicKeyP apub; - -}; - - -/** - * Information about a signing key on offer by the esign helper. - */ -struct HelperSignkey -{ - /** - * When will the helper start to use this key for signing? - */ - struct GNUNET_TIME_Timestamp start_time; - - /** - * For how long will the helper allow signing? 0 if - * the key was revoked or purged. - */ - struct GNUNET_TIME_Relative validity_duration; - - /** - * The public key. - */ - struct TALER_ExchangePublicKeyP exchange_pub; - - /** - * Signature over this key from the security module's key. - */ - struct TALER_SecurityModuleSignatureP sm_sig; - -}; - - -/** - * State associated with the crypto helpers / security modules. NOT updated - * when the #key_generation is updated (instead constantly kept in sync - * whenever #TEH_keys_get_state() is called). - */ -struct HelperState -{ - - /** - * Handle for the esign/EdDSA helper. - */ - struct TALER_CRYPTO_ExchangeSignHelper *esh; - - /** - * Handle for the denom/RSA helper. - */ - struct TALER_CRYPTO_RsaDenominationHelper *rsadh; - - /** - * Handle for the denom/CS helper. - */ - struct TALER_CRYPTO_CsDenominationHelper *csdh; - - /** - * Map from H(denom_pub) to `struct HelperDenomination` entries. - */ - struct GNUNET_CONTAINER_MultiHashMap *denom_keys; - - /** - * Map from H(rsa_pub) to `struct HelperDenomination` entries. - */ - struct GNUNET_CONTAINER_MultiHashMap *rsa_keys; - - /** - * Map from H(cs_pub) to `struct HelperDenomination` entries. - */ - struct GNUNET_CONTAINER_MultiHashMap *cs_keys; - - /** - * Map from `struct TALER_ExchangePublicKey` to `struct HelperSignkey` - * entries. Based on the fact that a `struct GNUNET_PeerIdentity` is also - * an EdDSA public key. - */ - struct GNUNET_CONTAINER_MultiPeerMap *esign_keys; - -}; - - -/** - * Information we track for the crypto helpers. Preserved - * when the @e key_generation changes, thus kept separate. - */ -static struct HelperState helpers; - - -/** - * Entry in (sorted) array with possible pre-build responses for /keys. - * We keep pre-build responses for the various (valid) cherry-picking - * values around. - */ -struct KeysResponseData -{ - - /** - * Response to return if the client supports (deflate) compression. - */ - struct MHD_Response *response_compressed; - - /** - * Response to return if the client does not support compression. - */ - struct MHD_Response *response_uncompressed; - - /** - * ETag for these responses. - */ - char *etag; - - /** - * Cherry-picking timestamp the client must have set for this - * response to be valid. 0 if this is the "full" response. - * The client's request must include this date or a higher one - * for this response to be applicable. - */ - struct GNUNET_TIME_Timestamp cherry_pick_date; - -}; - - -/** - * @brief All information about an exchange online signing key (which is used to - * sign messages from the exchange). - */ -struct SigningKey -{ - - /** - * The exchange's (online signing) public key. - */ - struct TALER_ExchangePublicKeyP exchange_pub; - - /** - * Meta data about the signing key, such as validity periods. - */ - struct TALER_EXCHANGEDB_SignkeyMetaData meta; - - /** - * The long-term offline master key's signature for this signing key. - * Signs over @e exchange_pub and @e meta. - */ - struct TALER_MasterSignatureP master_sig; - -}; - -struct TEH_KeyStateHandle -{ - - /** - * Mapping from denomination keys to denomination key issue struct. - * Used to lookup the key by hash. - */ - struct GNUNET_CONTAINER_MultiHashMap *denomkey_map; - - /** - * Mapping from serial ID's to denomination key issue struct. - * Used to lookup the key by serial ID. - * - * FIXME: We need a 64-bit version of this in GNUNET. - */ - struct GNUNET_CONTAINER_MultiHashMap32 *denomserial_map; - - /** - * Map from `struct TALER_ExchangePublicKey` to `struct SigningKey` - * entries. Based on the fact that a `struct GNUNET_PeerIdentity` is also - * an EdDSA public key. - */ - struct GNUNET_CONTAINER_MultiPeerMap *signkey_map; - - /** - * Head of DLL of our global fees. - */ - struct TEH_GlobalFee *gf_head; - - /** - * Tail of DLL of our global fees. - */ - struct TEH_GlobalFee *gf_tail; - - /** - * json array with the auditors of this exchange. Contains exactly - * the information needed for the "auditors" field of the /keys response. - */ - json_t *auditors; - - /** - * json array with the global fees of this exchange. Contains exactly - * the information needed for the "global_fees" field of the /keys response. - */ - json_t *global_fees; - - /** - * Sorted array of responses to /keys (MUST be sorted by cherry-picking date) of - * length @e krd_array_length; - */ - struct KeysResponseData *krd_array; - - /** - * Length of the @e krd_array. - */ - unsigned int krd_array_length; - - /** - * Cached reply for a GET /management/keys request. Used so we do not - * re-create the reply every time. - */ - json_t *management_keys_reply; - - /** - * For which (global) key_generation was this data structure created? - * Used to check when we are outdated and need to be re-generated. - */ - uint64_t key_generation; - - /** - * When did we initiate the key reloading? - */ - struct GNUNET_TIME_Timestamp reload_time; - - /** - * What is the period at which we rotate keys - * (signing or denomination keys)? - */ - struct GNUNET_TIME_Relative rekey_frequency; - - /** - * When does our online signing key expire and we - * thus need to re-generate this response? - */ - struct GNUNET_TIME_Timestamp signature_expires; - - /** - * True if #finish_keys_response() was not yet run and this key state - * is only suitable for the /management/keys API. - */ - bool management_only; - -}; - - -/** - * Entry of /keys requests that are currently suspended because we are - * waiting for /keys to become ready. - */ -struct SuspendedKeysRequests -{ - /** - * Kept in a DLL. - */ - struct SuspendedKeysRequests *next; - - /** - * Kept in a DLL. - */ - struct SuspendedKeysRequests *prev; - - /** - * The suspended connection. - */ - struct MHD_Connection *connection; - - /** - * When does this request timeout? - */ - struct GNUNET_TIME_Absolute timeout; -}; - - -/** - * Information we track about wire fees. - */ -struct WireFeeSet -{ - - /** - * Kept in a DLL. - */ - struct WireFeeSet *next; - - /** - * Kept in a DLL. - */ - struct WireFeeSet *prev; - - /** - * Actual fees. - */ - struct TALER_WireFeeSet fees; - - /** - * Start date of fee validity (inclusive). - */ - struct GNUNET_TIME_Timestamp start_date; - - /** - * End date of fee validity (exclusive). - */ - struct GNUNET_TIME_Timestamp end_date; - - /** - * Wire method the fees apply to. - */ - char *method; -}; - - -/** - * State we keep per thread to cache the wire part of the /keys response. - */ -struct WireStateHandle -{ - - /** - * JSON reply for wire response. - */ - json_t *json_reply; - - /** - * ETag for this response (if any). - */ - char *etag; - - /** - * head of DLL of wire fees. - */ - struct WireFeeSet *wfs_head; - - /** - * Tail of DLL of wire fees. - */ - struct WireFeeSet *wfs_tail; - - /** - * Earliest timestamp of all the wire methods when we have no more fees. - */ - struct GNUNET_TIME_Absolute cache_expiration; - - /** - * @e cache_expiration time, formatted. - */ - char dat[128]; - - /** - * For which (global) wire_generation was this data structure created? - * Used to check when we are outdated and need to be re-generated. - */ - uint64_t wire_generation; - - /** - * Is the wire data ready? - */ - bool ready; - -}; - - -/** - * Stores the latest generation of our wire response. - */ -static struct WireStateHandle *wire_state; - -/** - * Handler listening for wire updates by other exchange - * services. - */ -static struct GNUNET_DB_EventHandler *wire_eh; - -/** - * Counter incremented whenever we have a reason to re-build the #wire_state - * because something external changed. - */ -static uint64_t wire_generation; - - -/** - * Stores the latest generation of our key state. - */ -static struct TEH_KeyStateHandle *key_state; - -/** - * Counter incremented whenever we have a reason to re-build the keys because - * something external changed. See #TEH_keys_get_state() and - * #TEH_keys_update_states() for uses of this variable. - */ -static uint64_t key_generation; - -/** - * Handler listening for wire updates by other exchange - * services. - */ -static struct GNUNET_DB_EventHandler *keys_eh; - -/** - * Head of DLL of suspended /keys requests. - */ -static struct SuspendedKeysRequests *skr_head; - -/** - * Tail of DLL of suspended /keys requests. - */ -static struct SuspendedKeysRequests *skr_tail; - -/** - * Number of entries in the @e skr_head DLL. - */ -static unsigned int skr_size; - -/** - * Task to force timeouts on /keys requests. - */ -static struct GNUNET_SCHEDULER_Task *keys_tt; - -/** - * For how long should a signing key be legally retained? - * Configuration value. - */ -static struct GNUNET_TIME_Relative signkey_legal_duration; - -/** - * What type of asset are we dealing with here? - */ -static char *asset_type; - -/** - * RSA security module public key, all zero if not known. - */ -static struct TALER_SecurityModulePublicKeyP denom_rsa_sm_pub; - -/** - * CS security module public key, all zero if not known. - */ -static struct TALER_SecurityModulePublicKeyP denom_cs_sm_pub; - -/** - * EdDSA security module public key, all zero if not known. - */ -static struct TALER_SecurityModulePublicKeyP esign_sm_pub; - -/** - * Are we shutting down? - */ -static bool terminating; - - -/** - * Free memory associated with @a wsh - * - * @param[in] wsh wire state to destroy - */ -static void -destroy_wire_state (struct WireStateHandle *wsh) -{ - struct WireFeeSet *wfs; - - while (NULL != (wfs = wsh->wfs_head)) - { - GNUNET_CONTAINER_DLL_remove (wsh->wfs_head, - wsh->wfs_tail, - wfs); - GNUNET_free (wfs->method); - GNUNET_free (wfs); - } - json_decref (wsh->json_reply); - GNUNET_free (wsh->etag); - GNUNET_free (wsh); -} - - -/** - * Function called whenever another exchange process has updated - * the wire data in the database. - * - * @param cls NULL - * @param extra unused - * @param extra_size number of bytes in @a extra unused - */ -static void -wire_update_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - (void) cls; - (void) extra; - (void) extra_size; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received wire update event\n"); - TEH_check_invariants (); - wire_generation++; - key_generation++; - TEH_resume_keys_requests (false); -} - - -/** - * Add information about a wire account to @a cls. - * - * @param cls a `json_t *` array to expand with wire account details - * @param payto_uri the exchange bank account URI to add - * @param conversion_url URL of a conversion service, NULL if there is no conversion - * @param open_banking_gateway URL of an open banking gateway, NULL if there is none - * @param wire_transfer_gateway URL of a wire transfer gateway, NULL if there is none - * @param debit_restrictions JSON array with debit restrictions on the account - * @param credit_restrictions JSON array with credit restrictions on the account - * @param master_sig master key signature affirming that this is a bank - * account of the exchange (of purpose #TALER_SIGNATURE_MASTER_WIRE_DETAILS) - * @param bank_label label the wallet should use to display the account, can be NULL - * @param priority priority for ordering bank account labels - */ -static void -add_wire_account ( - void *cls, - const struct TALER_FullPayto payto_uri, - const char *conversion_url, - const char *open_banking_gateway, - const char *wire_transfer_gateway, - const json_t *debit_restrictions, - const json_t *credit_restrictions, - const struct TALER_MasterSignatureP *master_sig, - const char *bank_label, - int64_t priority) -{ - json_t *a = cls; - - if (GNUNET_OK != - TALER_exchange_wire_signature_check ( - payto_uri, - conversion_url, - open_banking_gateway, - wire_transfer_gateway, - debit_restrictions, - credit_restrictions, - &TEH_master_public_key, - master_sig)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database has wire account with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); - return; - } - if (0 != - json_array_append_new ( - a, - GNUNET_JSON_PACK ( - TALER_JSON_pack_full_payto ( - "payto_uri", - payto_uri), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "conversion_url", - conversion_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "open_banking_gateway", - open_banking_gateway)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "wire_transfer_gateway", - wire_transfer_gateway)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "bank_label", - bank_label)), - GNUNET_JSON_pack_int64 ( - "priority", - priority), - GNUNET_JSON_pack_array_incref ( - "debit_restrictions", - (json_t *) debit_restrictions), - GNUNET_JSON_pack_array_incref ( - "credit_restrictions", - (json_t *) credit_restrictions), - GNUNET_JSON_pack_data_auto ( - "master_sig", - master_sig)))) - { - GNUNET_break (0); /* out of memory!? */ - return; - } -} - - -/** - * Closure for #add_wire_fee(). - */ -struct AddContext -{ - /** - * Wire method the fees are for. - */ - char *wire_method; - - /** - * Wire state we are building. - */ - struct WireStateHandle *wsh; - - /** - * Array to append the fee to. - */ - json_t *a; - - /** - * Set to the maximum end-date seen. - */ - struct GNUNET_TIME_Absolute max_seen; -}; - - -/** - * Add information about a wire account to @a cls. - * - * @param cls a `struct AddContext` - * @param fees the wire fees we charge - * @param start_date from when are these fees valid (start date) - * @param end_date until when are these fees valid (end date, exclusive) - * @param master_sig master key signature affirming that this is the correct - * fee (of purpose #TALER_SIGNATURE_MASTER_WIRE_FEES) - */ -static void -add_wire_fee (void *cls, - const struct TALER_WireFeeSet *fees, - struct GNUNET_TIME_Timestamp start_date, - struct GNUNET_TIME_Timestamp end_date, - const struct TALER_MasterSignatureP *master_sig) -{ - struct AddContext *ac = cls; - struct WireFeeSet *wfs; - - if (GNUNET_OK != - TALER_exchange_offline_wire_fee_verify ( - ac->wire_method, - start_date, - end_date, - fees, - &TEH_master_public_key, - master_sig)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database has wire fee with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); - return; - } - ac->max_seen = GNUNET_TIME_absolute_max (ac->max_seen, - end_date.abs_time); - wfs = GNUNET_new (struct WireFeeSet); - wfs->start_date = start_date; - wfs->end_date = end_date; - wfs->fees = *fees; - wfs->method = GNUNET_strdup (ac->wire_method); - GNUNET_CONTAINER_DLL_insert (ac->wsh->wfs_head, - ac->wsh->wfs_tail, - wfs); - if (0 != - json_array_append_new ( - ac->a, - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("wire_fee", - &fees->wire), - TALER_JSON_pack_amount ("closing_fee", - &fees->closing), - GNUNET_JSON_pack_timestamp ("start_date", - start_date), - GNUNET_JSON_pack_timestamp ("end_date", - end_date), - GNUNET_JSON_pack_data_auto ("sig", - master_sig)))) - { - GNUNET_break (0); /* out of memory!? */ - return; - } -} - - -/** - * Create the wire response from our database state. - * - * @return NULL on error - */ -static struct WireStateHandle * -build_wire_state (void) -{ - json_t *wire_accounts_array; - json_t *wire_fee_object; - uint64_t wg = wire_generation; /* must be obtained FIRST */ - enum GNUNET_DB_QueryStatus qs; - struct WireStateHandle *wsh; - json_t *wads; - - wsh = GNUNET_new (struct WireStateHandle); - wsh->wire_generation = wg; - wire_accounts_array = json_array (); - GNUNET_assert (NULL != wire_accounts_array); - qs = TEH_plugin->get_wire_accounts (TEH_plugin->cls, - &add_wire_account, - wire_accounts_array); - if (0 > qs) - { - GNUNET_break (0); - json_decref (wire_accounts_array); - wsh->ready = false; - return wsh; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Built wire data with %u accounts (%d)\n", - (unsigned int) json_array_size (wire_accounts_array), - (int) qs); - wire_fee_object = json_object (); - GNUNET_assert (NULL != wire_fee_object); - wsh->cache_expiration = GNUNET_TIME_UNIT_FOREVER_ABS; - { - json_t *account; - size_t index; - - json_array_foreach (wire_accounts_array, - index, - account) - { - char *wire_method; - const char *payto_uri = json_string_value (json_object_get (account, - "payto_uri")); - - GNUNET_assert (NULL != payto_uri); - wire_method = TALER_payto_get_method (payto_uri); - if (NULL == wire_method) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "No wire method in `%s'\n", - payto_uri); - wsh->ready = false; - json_decref (wire_accounts_array); - json_decref (wire_fee_object); - return wsh; - } - if (NULL == json_object_get (wire_fee_object, - wire_method)) - { - struct AddContext ac = { - .wire_method = wire_method, - .wsh = wsh, - .a = json_array () - }; - - GNUNET_assert (NULL != ac.a); - qs = TEH_plugin->get_wire_fees (TEH_plugin->cls, - wire_method, - &add_wire_fee, - &ac); - if (0 > qs) - { - GNUNET_break (0); - json_decref (ac.a); - json_decref (wire_fee_object); - json_decref (wire_accounts_array); - GNUNET_free (wire_method); - wsh->ready = false; - return wsh; - } - if (0 != json_array_size (ac.a)) - { - wsh->cache_expiration - = GNUNET_TIME_absolute_min (ac.max_seen, - wsh->cache_expiration); - GNUNET_assert (0 == - json_object_set_new (wire_fee_object, - wire_method, - ac.a)); - } - else - { - json_decref (ac.a); - } - } - GNUNET_free (wire_method); - } - } - - wads = json_array (); /* #7271 */ - GNUNET_assert (NULL != wads); - wsh->json_reply = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("accounts", - wire_accounts_array), - GNUNET_JSON_pack_array_steal ("wads", - wads), - GNUNET_JSON_pack_object_steal ("fees", - wire_fee_object)); - wsh->ready = true; - return wsh; -} - - -void -TEH_wire_update_state (void) -{ - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_EXCHANGE_WIRE_UPDATED), - }; - - TEH_plugin->event_notify (TEH_plugin->cls, - &es, - NULL, - 0); - wire_generation++; - key_generation++; -} - - -/** - * Return the current key state for this thread. Possibly - * re-builds the key state if we have reason to believe - * that something changed. - * - * @return NULL on error - */ -static struct WireStateHandle * -get_wire_state (void) -{ - struct WireStateHandle *old_wsh; - - old_wsh = wire_state; - if ( (NULL == old_wsh) || - (old_wsh->wire_generation < wire_generation) ) - { - struct WireStateHandle *wsh; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Rebuilding wire, generation upgrade from %llu to %llu\n", - (unsigned long long) (NULL == old_wsh) ? 0LL : - old_wsh->wire_generation, - (unsigned long long) wire_generation); - TEH_check_invariants (); - wsh = build_wire_state (); - wire_state = wsh; - if (NULL != old_wsh) - destroy_wire_state (old_wsh); - TEH_check_invariants (); - return wsh; - } - return old_wsh; -} - - -const struct TALER_WireFeeSet * -TEH_wire_fees_by_time ( - struct GNUNET_TIME_Timestamp ts, - const char *method) -{ - struct WireStateHandle *wsh = get_wire_state (); - - for (struct WireFeeSet *wfs = wsh->wfs_head; - NULL != wfs; - wfs = wfs->next) - { - if (0 != strcmp (method, - wfs->method)) - continue; - if ( (GNUNET_TIME_timestamp_cmp (wfs->start_date, - >, - ts)) || - (GNUNET_TIME_timestamp_cmp (ts, - >=, - wfs->end_date)) ) - continue; - return &wfs->fees; - } - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No wire fees for method `%s' at %s configured\n", - method, - GNUNET_TIME_timestamp2s (ts)); - return NULL; -} - - -/** - * Function called to forcefully resume suspended keys requests. - * - * @param cls unused, NULL - */ -static void -keys_timeout_cb (void *cls) -{ - struct SuspendedKeysRequests *skr; - - (void) cls; - keys_tt = NULL; - while (NULL != (skr = skr_head)) - { - if (GNUNET_TIME_absolute_is_future (skr->timeout)) - break; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming /keys request due to timeout\n"); - GNUNET_CONTAINER_DLL_remove (skr_head, - skr_tail, - skr); - skr_size--; - MHD_resume_connection (skr->connection); - TALER_MHD_daemon_trigger (); - GNUNET_free (skr); - } - if (NULL == skr) - return; - keys_tt = GNUNET_SCHEDULER_add_at (skr->timeout, - &keys_timeout_cb, - NULL); -} - - -/** - * Suspend /keys request while we (hopefully) are waiting to be - * provisioned with key material. - * - * @param[in] connection to suspend - */ -static MHD_RESULT -suspend_request (struct MHD_Connection *connection) -{ - struct SuspendedKeysRequests *skr; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending /keys request until key material changes\n"); - if (terminating) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - "Exchange terminating"); - } - skr = GNUNET_new (struct SuspendedKeysRequests); - skr->connection = connection; - MHD_suspend_connection (connection); - GNUNET_CONTAINER_DLL_insert (skr_head, - skr_tail, - skr); - skr_size++; - skr->timeout = GNUNET_TIME_relative_to_absolute (KEYS_TIMEOUT); - if (NULL == keys_tt) - { - keys_tt = GNUNET_SCHEDULER_add_at (skr->timeout, - &keys_timeout_cb, - NULL); - } - while (skr_size > SKR_LIMIT) - { - skr = skr_tail; - GNUNET_CONTAINER_DLL_remove (skr_head, - skr_tail, - skr); - skr_size--; - MHD_resume_connection (skr->connection); - TALER_MHD_daemon_trigger (); - GNUNET_free (skr); - } - return MHD_YES; -} - - -/** - * Called on each denomination key. Checks that the key still works. - * - * @param cls NULL - * @param hc denomination hash (unused) - * @param value a `struct TEH_DenominationKey` - * @return #GNUNET_OK - */ -static enum GNUNET_GenericReturnValue -check_dk (void *cls, - const struct GNUNET_HashCode *hc, - void *value) -{ - struct TEH_DenominationKey *dk = value; - - (void) cls; - (void) hc; - switch (dk->denom_pub.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_INVALID: - break; - case GNUNET_CRYPTO_BSA_RSA: - GNUNET_assert (GNUNET_CRYPTO_rsa_public_key_check ( - dk->denom_pub.bsign_pub_key->details.rsa_public_key)); - return GNUNET_OK; - case GNUNET_CRYPTO_BSA_CS: - /* nothing to do for GNUNET_CRYPTO_BSA_CS */ - return GNUNET_OK; - } - GNUNET_assert (0); - return GNUNET_SYSERR; -} - - -void -TEH_check_invariants () -{ - struct TEH_KeyStateHandle *ksh; - - if (0 == TEH_check_invariants_flag) - return; - ksh = TEH_keys_get_state (); - if (NULL == ksh) - return; - GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, - &check_dk, - NULL); -} - - -void -TEH_resume_keys_requests (bool do_shutdown) -{ - struct SuspendedKeysRequests *skr; - - if (do_shutdown) - terminating = true; - while (NULL != (skr = skr_head)) - { - GNUNET_CONTAINER_DLL_remove (skr_head, - skr_tail, - skr); - skr_size--; - MHD_resume_connection (skr->connection); - TALER_MHD_daemon_trigger (); - GNUNET_free (skr); - } - GNUNET_assert (0 == skr_size); -} - - -/** - * Clear memory for responses to "/keys" in @a ksh. - * - * @param[in,out] ksh key state to update - */ -static void -clear_response_cache (struct TEH_KeyStateHandle *ksh) -{ - for (unsigned int i = 0; i<ksh->krd_array_length; i++) - { - struct KeysResponseData *krd = &ksh->krd_array[i]; - - MHD_destroy_response (krd->response_compressed); - MHD_destroy_response (krd->response_uncompressed); - GNUNET_free (krd->etag); - } - GNUNET_array_grow (ksh->krd_array, - ksh->krd_array_length, - 0); -} - - -/** - * Check that the given RSA security module's public key is the one - * we have pinned. If it does not match, we die hard. - * - * @param sm_pub RSA security module public key to check - */ -static void -check_denom_rsa_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) -{ - if (0 != - GNUNET_memcmp (sm_pub, - &denom_rsa_sm_pub)) - { - if (! GNUNET_is_zero (&denom_rsa_sm_pub)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Our RSA security module changed its key. This must not happen.\n"); - GNUNET_assert (0); - } - denom_rsa_sm_pub = *sm_pub; /* TOFU ;-) */ - } -} - - -/** - * Check that the given CS security module's public key is the one - * we have pinned. If it does not match, we die hard. - * - * @param sm_pub RSA security module public key to check - */ -static void -check_denom_cs_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) -{ - if (0 != - GNUNET_memcmp (sm_pub, - &denom_cs_sm_pub)) - { - if (! GNUNET_is_zero (&denom_cs_sm_pub)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Our CS security module changed its key. This must not happen.\n"); - GNUNET_assert (0); - } - denom_cs_sm_pub = *sm_pub; /* TOFU ;-) */ - } -} - - -/** - * Check that the given EdDSA security module's public key is the one - * we have pinned. If it does not match, we die hard. - * - * @param sm_pub EdDSA security module public key to check - */ -static void -check_esign_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub) -{ - if (0 != - GNUNET_memcmp (sm_pub, - &esign_sm_pub)) - { - if (! GNUNET_is_zero (&esign_sm_pub)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Our EdDSA security module changed its key. This must not happen.\n"); - GNUNET_assert (0); - } - esign_sm_pub = *sm_pub; /* TOFU ;-) */ - } -} - - -/** - * Helper function for #destroy_key_helpers to free all entries - * in the `denom_keys` map. - * - * @param cls the `struct HelperDenomination` - * @param h_denom_pub hash of the denomination public key - * @param value the `struct HelperDenomination` to release - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -free_denom_cb (void *cls, - const struct GNUNET_HashCode *h_denom_pub, - void *value) -{ - struct HelperDenomination *hd = value; - - (void) cls; - (void) h_denom_pub; - TALER_denom_pub_free (&hd->denom_pub); - GNUNET_free (hd->section_name); - GNUNET_free (hd); - return GNUNET_OK; -} - - -/** - * Helper function for #destroy_key_helpers to free all entries - * in the `esign_keys` map. - * - * @param cls the `struct HelperSignkey` - * @param pid unused, matches the exchange public key - * @param value the `struct HelperSignkey` to release - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -free_esign_cb (void *cls, - const struct GNUNET_PeerIdentity *pid, - void *value) -{ - struct HelperSignkey *hsk = value; - - (void) cls; - (void) pid; - GNUNET_free (hsk); - return GNUNET_OK; -} - - -/** - * Destroy helper state. Does NOT call free() on @a hs, as that - * state is not separately allocated! Dual to #setup_key_helpers(). - * - * @param[in] hs helper state to free, but NOT the @a hs pointer itself! - */ -static void -destroy_key_helpers (struct HelperState *hs) -{ - GNUNET_CONTAINER_multihashmap_iterate (hs->denom_keys, - &free_denom_cb, - hs); - GNUNET_CONTAINER_multihashmap_destroy (hs->rsa_keys); - hs->rsa_keys = NULL; - GNUNET_CONTAINER_multihashmap_destroy (hs->cs_keys); - hs->cs_keys = NULL; - GNUNET_CONTAINER_multihashmap_destroy (hs->denom_keys); - hs->denom_keys = NULL; - GNUNET_CONTAINER_multipeermap_iterate (hs->esign_keys, - &free_esign_cb, - hs); - GNUNET_CONTAINER_multipeermap_destroy (hs->esign_keys); - hs->esign_keys = NULL; - if (NULL != hs->rsadh) - { - TALER_CRYPTO_helper_rsa_disconnect (hs->rsadh); - hs->rsadh = NULL; - } - if (NULL != hs->csdh) - { - TALER_CRYPTO_helper_cs_disconnect (hs->csdh); - hs->csdh = NULL; - } - if (NULL != hs->esh) - { - TALER_CRYPTO_helper_esign_disconnect (hs->esh); - hs->esh = NULL; - } -} - - -/** - * Looks up the AGE_RESTRICTED setting for a denomination in the config and - * returns the age restriction (mask) accordingly. - * - * @param section_name Section in the configuration for the particular - * denomination. - */ -static struct TALER_AgeMask -load_age_mask (const char *section_name) -{ - static const struct TALER_AgeMask null_mask = {0}; - enum GNUNET_GenericReturnValue ret; - - if (GNUNET_OK != (GNUNET_CONFIGURATION_have_value ( - TEH_cfg, - section_name, - "AGE_RESTRICTED"))) - return null_mask; - - if (GNUNET_SYSERR == - (ret = GNUNET_CONFIGURATION_get_value_yesno (TEH_cfg, - section_name, - "AGE_RESTRICTED"))) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - section_name, - "AGE_RESTRICTED", - "Value must be YES or NO\n"); - return null_mask; - } - - if (GNUNET_OK == ret) - { - if (! TEH_age_restriction_enabled) - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "age restriction set in section %s, yet, age restriction is not enabled\n", - section_name); - return TEH_age_restriction_config.mask; - } - - - return null_mask; -} - - -/** - * Function called with information about available keys for signing. Usually - * only called once per key upon connect. Also called again in case a key is - * being revoked, in that case with an @a end_time of zero. - * - * @param cls closure with the `struct HelperState *` - * @param section_name name of the denomination type in the configuration; - * NULL if the key has been revoked or purged - * @param start_time when does the key become available for signing; - * zero if the key has been revoked or purged - * @param validity_duration how long does the key remain available for signing; - * zero if the key has been revoked or purged - * @param h_rsa hash of the @a denom_pub that is available (or was purged) - * @param bs_pub the public key itself, NULL if the key was revoked or purged - * @param sm_pub public key of the security module, NULL if the key was revoked or purged - * @param sm_sig signature from the security module - */ -static void -helper_rsa_cb ( - void *cls, - const char *section_name, - struct GNUNET_TIME_Timestamp start_time, - struct GNUNET_TIME_Relative validity_duration, - const struct TALER_RsaPubHashP *h_rsa, - struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub, - const struct TALER_SecurityModulePublicKeyP *sm_pub, - const struct TALER_SecurityModuleSignatureP *sm_sig) -{ - struct HelperState *hs = cls; - struct HelperDenomination *hd; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "RSA helper announces key %s for denomination type %s with validity %s\n", - GNUNET_h2s (&h_rsa->hash), - section_name, - GNUNET_STRINGS_relative_time_to_string (validity_duration, - GNUNET_NO)); - key_generation++; - TEH_resume_keys_requests (false); - hd = GNUNET_CONTAINER_multihashmap_get (hs->rsa_keys, - &h_rsa->hash); - if (NULL != hd) - { - /* should be just an update (revocation!), so update existing entry */ - hd->validity_duration = validity_duration; - return; - } - GNUNET_assert (NULL != sm_pub); - check_denom_rsa_sm_pub (sm_pub); - hd = GNUNET_new (struct HelperDenomination); - hd->start_time = start_time; - hd->validity_duration = validity_duration; - hd->h_details.h_rsa = *h_rsa; - hd->sm_sig = *sm_sig; - GNUNET_assert (GNUNET_CRYPTO_BSA_RSA == bs_pub->cipher); - hd->denom_pub.bsign_pub_key = - GNUNET_CRYPTO_bsign_pub_incref (bs_pub); - /* load the age mask for the denomination, if applicable */ - hd->denom_pub.age_mask = load_age_mask (section_name); - TALER_denom_pub_hash (&hd->denom_pub, - &hd->h_denom_pub); - hd->section_name = GNUNET_strdup (section_name); - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap_put ( - hs->denom_keys, - &hd->h_denom_pub.hash, - hd, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap_put ( - hs->rsa_keys, - &hd->h_details.h_rsa.hash, - hd, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); -} - - -/** - * Function called with information about available CS keys for signing. Usually - * only called once per key upon connect. Also called again in case a key is - * being revoked, in that case with an @a end_time of zero. - * - * @param cls closure with the `struct HelperState *` - * @param section_name name of the denomination type in the configuration; - * NULL if the key has been revoked or purged - * @param start_time when does the key become available for signing; - * zero if the key has been revoked or purged - * @param validity_duration how long does the key remain available for signing; - * zero if the key has been revoked or purged - * @param h_cs hash of the @a denom_pub that is available (or was purged) - * @param bs_pub the public key itself, NULL if the key was revoked or purged - * @param sm_pub public key of the security module, NULL if the key was revoked or purged - * @param sm_sig signature from the security module - */ -static void -helper_cs_cb ( - void *cls, - const char *section_name, - struct GNUNET_TIME_Timestamp start_time, - struct GNUNET_TIME_Relative validity_duration, - const struct TALER_CsPubHashP *h_cs, - struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub, - const struct TALER_SecurityModulePublicKeyP *sm_pub, - const struct TALER_SecurityModuleSignatureP *sm_sig) -{ - struct HelperState *hs = cls; - struct HelperDenomination *hd; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "CS helper announces key %s for denomination type %s with validity %s\n", - GNUNET_h2s (&h_cs->hash), - section_name, - GNUNET_STRINGS_relative_time_to_string (validity_duration, - GNUNET_NO)); - key_generation++; - TEH_resume_keys_requests (false); - hd = GNUNET_CONTAINER_multihashmap_get (hs->cs_keys, - &h_cs->hash); - if (NULL != hd) - { - /* should be just an update (revocation!), so update existing entry */ - hd->validity_duration = validity_duration; - return; - } - GNUNET_assert (NULL != sm_pub); - check_denom_cs_sm_pub (sm_pub); - hd = GNUNET_new (struct HelperDenomination); - hd->start_time = start_time; - hd->validity_duration = validity_duration; - hd->h_details.h_cs = *h_cs; - hd->sm_sig = *sm_sig; - GNUNET_assert (GNUNET_CRYPTO_BSA_CS == bs_pub->cipher); - hd->denom_pub.bsign_pub_key - = GNUNET_CRYPTO_bsign_pub_incref (bs_pub); - /* load the age mask for the denomination, if applicable */ - hd->denom_pub.age_mask = load_age_mask (section_name); - TALER_denom_pub_hash (&hd->denom_pub, - &hd->h_denom_pub); - hd->section_name = GNUNET_strdup (section_name); - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap_put ( - hs->denom_keys, - &hd->h_denom_pub.hash, - hd, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap_put ( - hs->cs_keys, - &hd->h_details.h_cs.hash, - hd, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); -} - - -/** - * Function called with information about available keys for signing. Usually - * only called once per key upon connect. Also called again in case a key is - * being revoked, in that case with an @a end_time of zero. - * - * @param cls closure with the `struct HelperState *` - * @param start_time when does the key become available for signing; - * zero if the key has been revoked or purged - * @param validity_duration how long does the key remain available for signing; - * zero if the key has been revoked or purged - * @param exchange_pub the public key itself, NULL if the key was revoked or purged - * @param sm_pub public key of the security module, NULL if the key was revoked or purged - * @param sm_sig signature from the security module - */ -static void -helper_esign_cb ( - void *cls, - struct GNUNET_TIME_Timestamp start_time, - struct GNUNET_TIME_Relative validity_duration, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_SecurityModulePublicKeyP *sm_pub, - const struct TALER_SecurityModuleSignatureP *sm_sig) -{ - struct HelperState *hs = cls; - struct HelperSignkey *hsk; - struct GNUNET_PeerIdentity pid; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "EdDSA helper announces signing key %s with validity %s\n", - TALER_B2S (exchange_pub), - GNUNET_STRINGS_relative_time_to_string (validity_duration, - GNUNET_NO)); - key_generation++; - TEH_resume_keys_requests (false); - pid.public_key = exchange_pub->eddsa_pub; - hsk = GNUNET_CONTAINER_multipeermap_get (hs->esign_keys, - &pid); - if (NULL != hsk) - { - /* should be just an update (revocation!), so update existing entry */ - hsk->validity_duration = validity_duration; - return; - } - GNUNET_assert (NULL != sm_pub); - check_esign_sm_pub (sm_pub); - hsk = GNUNET_new (struct HelperSignkey); - hsk->start_time = start_time; - hsk->validity_duration = validity_duration; - hsk->exchange_pub = *exchange_pub; - hsk->sm_sig = *sm_sig; - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multipeermap_put ( - hs->esign_keys, - &pid, - hsk, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); -} - - -/** - * Setup helper state. - * - * @param[out] hs helper state to initialize - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -setup_key_helpers (struct HelperState *hs) -{ - hs->denom_keys - = GNUNET_CONTAINER_multihashmap_create (1024, - GNUNET_YES); - hs->rsa_keys - = GNUNET_CONTAINER_multihashmap_create (1024, - GNUNET_YES); - hs->cs_keys - = GNUNET_CONTAINER_multihashmap_create (1024, - GNUNET_YES); - hs->esign_keys - = GNUNET_CONTAINER_multipeermap_create (32, - GNUNET_NO /* MUST BE NO! */); - hs->rsadh = TALER_CRYPTO_helper_rsa_connect (TEH_cfg, - "taler-exchange", - &helper_rsa_cb, - hs); - if (NULL == hs->rsadh) - return GNUNET_SYSERR; - hs->csdh = TALER_CRYPTO_helper_cs_connect (TEH_cfg, - "taler-exchange", - &helper_cs_cb, - hs); - if (NULL == hs->csdh) - return GNUNET_SYSERR; - hs->esh = TALER_CRYPTO_helper_esign_connect (TEH_cfg, - "taler-exchange", - &helper_esign_cb, - hs); - if (NULL == hs->esh) - return GNUNET_SYSERR; - return GNUNET_OK; -} - - -/** - * Synchronize helper state. Polls the key helper for updates. - * - * @param[in,out] hs helper state to synchronize - */ -static void -sync_key_helpers (struct HelperState *hs) -{ - TALER_CRYPTO_helper_rsa_poll (hs->rsadh); - TALER_CRYPTO_helper_cs_poll (hs->csdh); - TALER_CRYPTO_helper_esign_poll (hs->esh); -} - - -/** - * Free denomination key data. - * - * @param cls a `struct TEH_KeyStateHandle`, unused - * @param h_denom_pub hash of the denomination public key, unused - * @param value a `struct TEH_DenominationKey` to free - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -clear_denomination_cb (void *cls, - const struct GNUNET_HashCode *h_denom_pub, - void *value) -{ - struct TEH_DenominationKey *dk = value; - struct TEH_AuditorSignature *as; - - (void) cls; - (void) h_denom_pub; - TALER_denom_pub_free (&dk->denom_pub); - while (NULL != (as = dk->as_head)) - { - GNUNET_CONTAINER_DLL_remove (dk->as_head, - dk->as_tail, - as); - GNUNET_free (as); - } - GNUNET_free (dk); - return GNUNET_OK; -} - - -/** - * Free denomination key data. - * - * @param cls a `struct TEH_KeyStateHandle`, unused - * @param pid the online signing key (type-disguised), unused - * @param value a `struct SigningKey` to free - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -clear_signkey_cb (void *cls, - const struct GNUNET_PeerIdentity *pid, - void *value) -{ - struct SigningKey *sk = value; - - (void) cls; - (void) pid; - GNUNET_free (sk); - return GNUNET_OK; -} - - -/** - * Free resources associated with @a cls, possibly excluding - * the helper data. - * - * @param[in] ksh key state to release - */ -static void -destroy_key_state (struct TEH_KeyStateHandle *ksh) -{ - struct TEH_GlobalFee *gf; - - clear_response_cache (ksh); - while (NULL != (gf = ksh->gf_head)) - { - GNUNET_CONTAINER_DLL_remove (ksh->gf_head, - ksh->gf_tail, - gf); - GNUNET_free (gf); - } - GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, - &clear_denomination_cb, - ksh); - GNUNET_CONTAINER_multihashmap_destroy (ksh->denomkey_map); - GNUNET_CONTAINER_multihashmap32_destroy (ksh->denomserial_map); - GNUNET_CONTAINER_multipeermap_iterate (ksh->signkey_map, - &clear_signkey_cb, - ksh); - GNUNET_CONTAINER_multipeermap_destroy (ksh->signkey_map); - json_decref (ksh->auditors); - ksh->auditors = NULL; - json_decref (ksh->global_fees); - ksh->global_fees = NULL; - if (NULL != ksh->management_keys_reply) - { - json_decref (ksh->management_keys_reply); - ksh->management_keys_reply = NULL; - } - GNUNET_free (ksh); -} - - -/** - * Function called whenever another exchange process has updated - * the keys data in the database. - * - * @param cls NULL - * @param extra unused - * @param extra_size number of bytes in @a extra unused - */ -static void -keys_update_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - (void) cls; - (void) extra; - (void) extra_size; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received /keys update event\n"); - TEH_check_invariants (); - key_generation++; - TEH_resume_keys_requests (false); - TEH_check_invariants (); -} - - -enum GNUNET_GenericReturnValue -TEH_keys_init () -{ - if (GNUNET_OK != - setup_key_helpers (&helpers)) - { - destroy_key_helpers (&helpers); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (TEH_cfg, - "exchange", - "SIGNKEY_LEGAL_DURATION", - &signkey_legal_duration)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "exchange", - "SIGNKEY_LEGAL_DURATION"); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (TEH_cfg, - "exchange", - "ASSET_TYPE", - &asset_type)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, - "exchange", - "ASSET_TYPE"); - asset_type = GNUNET_strdup ("fiat"); - } - { - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), - }; - - keys_eh = TEH_plugin->event_listen (TEH_plugin->cls, - GNUNET_TIME_UNIT_FOREVER_REL, - &es, - &keys_update_event_cb, - NULL); - if (NULL == keys_eh) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - } - { - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), - }; - - wire_eh = TEH_plugin->event_listen (TEH_plugin->cls, - GNUNET_TIME_UNIT_FOREVER_REL, - &es, - &wire_update_event_cb, - NULL); - if (NULL == wire_eh) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - } - return GNUNET_OK; -} - - -/** - * Fully clean up our state. - */ -void -TEH_keys_finished () -{ - if (NULL != wire_state) - { - destroy_wire_state (wire_state); - wire_state = NULL; - } - if (NULL != wire_eh) - { - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - wire_eh); - wire_eh = NULL; - } - if (NULL != keys_tt) - { - GNUNET_SCHEDULER_cancel (keys_tt); - keys_tt = NULL; - } - if (NULL != key_state) - destroy_key_state (key_state); - if (NULL != keys_eh) - { - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - keys_eh); - keys_eh = NULL; - } - destroy_key_helpers (&helpers); -} - - -/** - * Function called with information about the exchange's denomination keys. - * - * @param cls closure with a `struct TEH_KeyStateHandle *` - * @param denom_pub public key of the denomination - * @param h_denom_pub hash of @a denom_pub - * @param meta meta data information about the denomination type (value, expirations, fees) - * @param master_sig master signature affirming the validity of this denomination - * @param recoup_possible true if the key was revoked and clients can currently recoup - * coins of this denomination - */ -static void -denomination_info_cb ( - void *cls, - const struct TALER_DenominationPublicKey *denom_pub, - const struct TALER_DenominationHashP *h_denom_pub, - const struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta, - const struct TALER_MasterSignatureP *master_sig, - bool recoup_possible) -{ - struct TEH_KeyStateHandle *ksh = cls; - struct TEH_DenominationKey *dk; - - if (GNUNET_TIME_absolute_is_past (meta->expire_deposit.abs_time)) - { - /* should have been filtered by DB query already! */ - GNUNET_break (0); - return; - } - if (GNUNET_OK != - TALER_exchange_offline_denom_validity_verify ( - h_denom_pub, - meta->start, - meta->expire_withdraw, - meta->expire_deposit, - meta->expire_legal, - &meta->value, - &meta->fees, - &TEH_master_public_key, - master_sig)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database has denomination with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); - return; - } - - GNUNET_assert (GNUNET_CRYPTO_BSA_INVALID != - denom_pub->bsign_pub_key->cipher); - if (GNUNET_TIME_absolute_is_zero (meta->start.abs_time) || - GNUNET_TIME_absolute_is_zero (meta->expire_withdraw.abs_time) || - GNUNET_TIME_absolute_is_zero (meta->expire_deposit.abs_time) || - GNUNET_TIME_absolute_is_zero (meta->expire_legal.abs_time) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database contains invalid denomination key %s\n", - GNUNET_h2s (&h_denom_pub->hash)); - return; - } - dk = GNUNET_new (struct TEH_DenominationKey); - TALER_denom_pub_copy (&dk->denom_pub, - denom_pub); - dk->h_denom_pub = *h_denom_pub; - dk->meta = *meta; - dk->master_sig = *master_sig; - dk->recoup_possible = recoup_possible; - dk->denom_pub.age_mask = meta->age_mask; - - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap_put (ksh->denomkey_map, - &dk->h_denom_pub.hash, - dk, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - { - uint32_t serial32 = (uint32_t) dk->meta.serial; - - GNUNET_assert (dk->meta.serial == (uint64_t) serial32); - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap32_put (ksh->denomserial_map, - serial32, - dk, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - } -} - - -/** - * Function called with information about the exchange's online signing keys. - * - * @param cls closure with a `struct TEH_KeyStateHandle *` - * @param exchange_pub the public key - * @param meta meta data information about the denomination type (expirations) - * @param master_sig master signature affirming the validity of this denomination - */ -static void -signkey_info_cb ( - void *cls, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_EXCHANGEDB_SignkeyMetaData *meta, - const struct TALER_MasterSignatureP *master_sig) -{ - struct TEH_KeyStateHandle *ksh = cls; - struct SigningKey *sk; - struct GNUNET_PeerIdentity pid; - - if (GNUNET_OK != - TALER_exchange_offline_signkey_validity_verify ( - exchange_pub, - meta->start, - meta->expire_sign, - meta->expire_legal, - &TEH_master_public_key, - master_sig)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database has signing key with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); - return; - } - sk = GNUNET_new (struct SigningKey); - sk->exchange_pub = *exchange_pub; - sk->meta = *meta; - sk->master_sig = *master_sig; - pid.public_key = exchange_pub->eddsa_pub; - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multipeermap_put (ksh->signkey_map, - &pid, - sk, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); -} - - -/** - * Closure for #get_auditor_sigs. - */ -struct GetAuditorSigsContext -{ - /** - * Where to store the matching signatures. - */ - json_t *denom_keys; - - /** - * Public key of the auditor to match against. - */ - const struct TALER_AuditorPublicKeyP *auditor_pub; -}; - - -/** - * Extract the auditor signatures matching the auditor's public - * key from the @a value and generate the respective JSON. - * - * @param cls a `struct GetAuditorSigsContext` - * @param h_denom_pub hash of the denomination public key - * @param value a `struct TEH_DenominationKey` - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -get_auditor_sigs (void *cls, - const struct GNUNET_HashCode *h_denom_pub, - void *value) -{ - struct GetAuditorSigsContext *ctx = cls; - struct TEH_DenominationKey *dk = value; - - for (struct TEH_AuditorSignature *as = dk->as_head; - NULL != as; - as = as->next) - { - if (0 != - GNUNET_memcmp (ctx->auditor_pub, - &as->apub)) - continue; - GNUNET_break (0 == - json_array_append_new ( - ctx->denom_keys, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("denom_pub_h", - h_denom_pub), - GNUNET_JSON_pack_data_auto ("auditor_sig", - &as->asig)))); - } - return GNUNET_OK; -} - - -/** - * Function called with information about the exchange's auditors. - * - * @param cls closure with a `struct TEH_KeyStateHandle *` - * @param auditor_pub the public key of the auditor - * @param auditor_url URL of the REST API of the auditor - * @param auditor_name human readable official name of the auditor - */ -static void -auditor_info_cb ( - void *cls, - const struct TALER_AuditorPublicKeyP *auditor_pub, - const char *auditor_url, - const char *auditor_name) -{ - struct TEH_KeyStateHandle *ksh = cls; - struct GetAuditorSigsContext ctx; - - ctx.denom_keys = json_array (); - GNUNET_assert (NULL != ctx.denom_keys); - ctx.auditor_pub = auditor_pub; - GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, - &get_auditor_sigs, - &ctx); - GNUNET_break (0 == - json_array_append_new ( - ksh->auditors, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("auditor_name", - auditor_name), - GNUNET_JSON_pack_data_auto ("auditor_pub", - auditor_pub), - GNUNET_JSON_pack_string ("auditor_url", - auditor_url), - GNUNET_JSON_pack_array_steal ("denomination_keys", - ctx.denom_keys)))); -} - - -/** - * Function called with information about the denominations - * audited by the exchange's auditors. - * - * @param cls closure with a `struct TEH_KeyStateHandle *` - * @param auditor_pub the public key of an auditor - * @param h_denom_pub hash of a denomination key audited by this auditor - * @param auditor_sig signature from the auditor affirming this - */ -static void -auditor_denom_cb ( - void *cls, - const struct TALER_AuditorPublicKeyP *auditor_pub, - const struct TALER_DenominationHashP *h_denom_pub, - const struct TALER_AuditorSignatureP *auditor_sig) -{ - struct TEH_KeyStateHandle *ksh = cls; - struct TEH_DenominationKey *dk; - struct TEH_AuditorSignature *as; - - dk = GNUNET_CONTAINER_multihashmap_get (ksh->denomkey_map, - &h_denom_pub->hash); - if (NULL == dk) - { - /* Odd, this should be impossible as per foreign key - constraint on 'auditor_denom_sigs'! Well, we can - safely continue anyway, so let's just log it. */ - GNUNET_break (0); - return; - } - as = GNUNET_new (struct TEH_AuditorSignature); - as->asig = *auditor_sig; - as->apub = *auditor_pub; - GNUNET_CONTAINER_DLL_insert (dk->as_head, - dk->as_tail, - as); -} - - -/** - * Closure for #add_sign_key_cb. - */ -struct SignKeyCtx -{ - /** - * What is the current rotation frequency for signing keys. Updated. - */ - struct GNUNET_TIME_Relative min_sk_frequency; - - /** - * JSON array of signing keys (being created). - */ - json_t *signkeys; -}; - - -/** - * Function called for all signing keys, used to build up the - * respective JSON response. - * - * @param cls a `struct SignKeyCtx *` with the array to append keys to - * @param pid the exchange public key (in type disguise) - * @param value a `struct SigningKey` - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -add_sign_key_cb (void *cls, - const struct GNUNET_PeerIdentity *pid, - void *value) -{ - struct SignKeyCtx *ctx = cls; - struct SigningKey *sk = value; - - (void) pid; - if (GNUNET_TIME_absolute_is_future (sk->meta.expire_sign.abs_time)) - { - ctx->min_sk_frequency = - GNUNET_TIME_relative_min (ctx->min_sk_frequency, - GNUNET_TIME_absolute_get_difference ( - sk->meta.start.abs_time, - sk->meta.expire_sign.abs_time)); - } - GNUNET_assert ( - 0 == - json_array_append_new ( - ctx->signkeys, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ("stamp_start", - sk->meta.start), - GNUNET_JSON_pack_timestamp ("stamp_expire", - sk->meta.expire_sign), - GNUNET_JSON_pack_timestamp ("stamp_end", - sk->meta.expire_legal), - GNUNET_JSON_pack_data_auto ("master_sig", - &sk->master_sig), - GNUNET_JSON_pack_data_auto ("key", - &sk->exchange_pub)))); - return GNUNET_OK; -} - - -/** - * Closure for #add_denom_key_cb. - */ -struct DenomKeyCtx -{ - /** - * Heap for sorting active denomination keys by start time. - */ - struct GNUNET_CONTAINER_Heap *heap; - - /** - * JSON array of revoked denomination keys. - */ - json_t *recoup; - - /** - * What is the minimum key rotation frequency of - * valid denomination keys? - */ - struct GNUNET_TIME_Relative min_dk_frequency; -}; - - -/** - * Function called for all denomination keys, used to build up the - * JSON list of *revoked* denomination keys and the - * heap of non-revoked denomination keys by timeout. - * - * @param cls a `struct DenomKeyCtx` - * @param h_denom_pub hash of the denomination key - * @param value a `struct TEH_DenominationKey` - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -add_denom_key_cb (void *cls, - const struct GNUNET_HashCode *h_denom_pub, - void *value) -{ - struct DenomKeyCtx *dkc = cls; - struct TEH_DenominationKey *dk = value; - - if (dk->recoup_possible) - { - GNUNET_assert ( - 0 == - json_array_append_new ( - dkc->recoup, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("h_denom_pub", - h_denom_pub)))); - } - else - { - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) - { - dkc->min_dk_frequency = - GNUNET_TIME_relative_min (dkc->min_dk_frequency, - GNUNET_TIME_absolute_get_difference ( - dk->meta.start.abs_time, - dk->meta.expire_withdraw.abs_time)); - } - (void) GNUNET_CONTAINER_heap_insert (dkc->heap, - dk, - dk->meta.start.abs_time.abs_value_us); - } - return GNUNET_OK; -} - - -/** - * Add the headers we want to set for every /keys response. - * - * @param cls the key state to use - * @param[in,out] response the response to modify - */ -static void -setup_general_response_headers (void *cls, - struct MHD_Response *response) -{ - struct TEH_KeyStateHandle *ksh = cls; - char dat[128]; - - TALER_MHD_add_global_headers (response, - true); - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_CONTENT_TYPE, - "application/json")); - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_CACHE_CONTROL, - "public,must-revalidate,max-age=86400") - ); - if (! GNUNET_TIME_relative_is_zero (ksh->rekey_frequency)) - { - struct GNUNET_TIME_Relative r; - struct GNUNET_TIME_Absolute a; - struct GNUNET_TIME_Timestamp km; - struct GNUNET_TIME_Timestamp m; - struct GNUNET_TIME_Timestamp we; - - r = GNUNET_TIME_relative_min (TEH_max_keys_caching, - ksh->rekey_frequency); - a = GNUNET_TIME_relative_to_absolute (r); - /* Round up to next full day to ensure the expiration - time does not become a fingerprint! */ - a = GNUNET_TIME_absolute_round_down (a, - GNUNET_TIME_UNIT_DAYS); - a = GNUNET_TIME_absolute_add (a, - GNUNET_TIME_UNIT_DAYS); - km = GNUNET_TIME_absolute_to_timestamp (a); - we = GNUNET_TIME_absolute_to_timestamp (wire_state->cache_expiration); - m = GNUNET_TIME_timestamp_min (we, - km); - TALER_MHD_get_date_string (m.abs_time, - dat); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Setting /keys 'Expires' header to '%s' (rekey frequency is %s)\n", - dat, - GNUNET_TIME_relative2s (ksh->rekey_frequency, - false)); - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_EXPIRES, - dat)); - ksh->signature_expires - = GNUNET_TIME_timestamp_min (m, - ksh->signature_expires); - } - /* Set cache control headers: our response varies depending on these headers */ - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_VARY, - MHD_HTTP_HEADER_ACCEPT_ENCODING)); -} - - -/** - * Initialize @a krd using the given values for @a signkeys, - * @a recoup and @a denoms. - * - * @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_cherry_pick_date timestamp to use - * @param[in,out] signkeys list of sign keys to return - * @param[in,out] recoup list of revoked keys to return - * @param[in,out] grouped_denominations list of grouped denominations to return - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -create_krd (struct TEH_KeyStateHandle *ksh, - const struct GNUNET_HashCode *denom_keys_hash, - struct GNUNET_TIME_Timestamp last_cherry_pick_date, - json_t *signkeys, - json_t *recoup, - json_t *grouped_denominations) -{ - struct KeysResponseData krd; - struct TALER_ExchangePublicKeyP exchange_pub; - struct TALER_ExchangeSignatureP exchange_sig; - struct WireStateHandle *wsh; - json_t *keys; - - wsh = get_wire_state (); - if (! wsh->ready) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - GNUNET_assert (! GNUNET_TIME_absolute_is_zero ( - last_cherry_pick_date.abs_time)); - GNUNET_assert (NULL != signkeys); - GNUNET_assert (NULL != recoup); - GNUNET_assert (NULL != grouped_denominations); - GNUNET_assert (NULL != ksh->auditors); - GNUNET_assert (NULL != TEH_currency); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Creating /keys at cherry pick date %s\n", - GNUNET_TIME_timestamp2s (last_cherry_pick_date)); - - /* Sign hash over master signatures of all denomination keys until this time - (in reverse order). */ - { - enum TALER_ErrorCode ec; - - if (TALER_EC_NONE != - (ec = - TALER_exchange_online_key_set_sign ( - &TEH_keys_exchange_sign2_, - ksh, - last_cherry_pick_date, - denom_keys_hash, - &exchange_pub, - &exchange_sig))) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Could not create key response data: cannot sign (%s)\n", - TALER_ErrorCode_get_hint (ec)); - return GNUNET_SYSERR; - } - } - - { - const struct SigningKey *sk; - - sk = GNUNET_CONTAINER_multipeermap_get ( - ksh->signkey_map, - (const struct GNUNET_PeerIdentity *) &exchange_pub); - ksh->signature_expires = GNUNET_TIME_timestamp_min (sk->meta.expire_sign, - ksh->signature_expires); - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Build /keys data with %u wire accounts\n", - (unsigned int) json_array_size ( - json_object_get (wsh->json_reply, - "accounts"))); - - keys = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("version", - EXCHANGE_PROTOCOL_VERSION), - GNUNET_JSON_pack_string ("base_url", - TEH_base_url), - GNUNET_JSON_pack_string ("currency", - TEH_currency), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "bank_compliance_language", - TEH_bank_compliance_language)), - GNUNET_JSON_pack_object_steal ( - "currency_specification", - TALER_JSON_currency_specs_to_json (TEH_cspec)), - GNUNET_JSON_pack_array_incref ( - "hard_limits", - TEH_hard_limits), - GNUNET_JSON_pack_array_incref ( - "zero_limits", - TEH_zero_limits), - TALER_JSON_pack_amount ("stefan_abs", - &TEH_stefan_abs), - TALER_JSON_pack_amount ("stefan_log", - &TEH_stefan_log), - GNUNET_JSON_pack_double ("stefan_lin", - (double) TEH_stefan_lin), - GNUNET_JSON_pack_string ("asset_type", - asset_type), - GNUNET_JSON_pack_bool ("kyc_enabled", - GNUNET_YES == TEH_enable_kyc), - GNUNET_JSON_pack_bool ("disable_direct_deposit", - GNUNET_YES == TEH_disable_direct_deposit), - GNUNET_JSON_pack_bool ("rewards_allowed", - false), - GNUNET_JSON_pack_data_auto ("master_public_key", - &TEH_master_public_key), - GNUNET_JSON_pack_time_rel ("reserve_closing_delay", - TEH_reserve_closing_delay), - GNUNET_JSON_pack_array_incref ("signkeys", - signkeys), - GNUNET_JSON_pack_array_incref ("recoup", - recoup), - GNUNET_JSON_pack_array_incref ("wads", - json_object_get (wsh->json_reply, - "wads")), - GNUNET_JSON_pack_array_incref ("accounts", - json_object_get (wsh->json_reply, - "accounts")), - GNUNET_JSON_pack_object_incref ("wire_fees", - json_object_get (wsh->json_reply, - "fees")), - GNUNET_JSON_pack_array_incref ("denominations", - grouped_denominations), - GNUNET_JSON_pack_array_incref ("auditors", - ksh->auditors), - GNUNET_JSON_pack_array_incref ("global_fees", - ksh->global_fees), - GNUNET_JSON_pack_timestamp ("list_issue_date", - last_cherry_pick_date), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ( - "wallet_balance_limit_without_kyc", - TALER_KYCLOGIC_get_wallet_thresholds ())), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("shopping_url", - TEH_shopping_url)), - TALER_JSON_pack_amount ("tiny_amount", - &TEH_tiny_amount), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &exchange_pub), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &exchange_sig)); - GNUNET_assert (NULL != keys); - /* Signal support for the configured, enabled extensions. */ - { - json_t *extensions = json_object (); - bool has_extensions = false; - - GNUNET_assert (NULL != extensions); - /* Fill in the configurations of the enabled extensions */ - for (const struct TALER_Extensions *iter = TALER_extensions_get_head (); - NULL != iter && NULL != iter->extension; - iter = iter->next) - { - const struct TALER_Extension *extension = iter->extension; - json_t *manifest; - int r; - - /* skip if not enabled */ - if (! extension->enabled) - continue; - - /* flag our findings so far */ - has_extensions = true; - - - manifest = extension->manifest (extension); - GNUNET_assert (manifest); - - r = json_object_set_new ( - extensions, - extension->name, - manifest); - GNUNET_assert (0 == r); - } - - /* Update the keys object with the extensions and its signature */ - if (has_extensions) - { - json_t *sig; - int r; - - r = json_object_set_new ( - keys, - "extensions", - extensions); - GNUNET_assert (0 == r); - - /* Add the signature of the extensions, if it is not zero */ - if (TEH_extensions_signed) - { - sig = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("extensions_sig", - &TEH_extensions_sig)); - - r = json_object_update (keys, sig); - GNUNET_assert (0 == r); - } - } - else - { - json_decref (extensions); - } - } - - - { - char *keys_json; - void *keys_jsonz; - size_t keys_jsonz_size; - int comp; - char etag[sizeof (struct GNUNET_HashCode) * 2]; - - /* Convert /keys response to UTF8-String */ - keys_json = json_dumps (keys, - JSON_INDENT (2)); - json_decref (keys); - GNUNET_assert (NULL != keys_json); - - /* Keep copy for later compression... */ - keys_jsonz = GNUNET_strdup (keys_json); - keys_jsonz_size = strlen (keys_json); - - /* hash to compute etag */ - { - struct GNUNET_HashCode ehash; - char *end; - - GNUNET_CRYPTO_hash (keys_jsonz, - keys_jsonz_size, - &ehash); - end = GNUNET_STRINGS_data_to_string (&ehash, - sizeof (ehash), - etag, - sizeof (etag)); - *end = '\0'; - } - - /* Create uncompressed response */ - krd.response_uncompressed - = MHD_create_response_from_buffer (keys_jsonz_size, - keys_json, - MHD_RESPMEM_MUST_FREE); - GNUNET_assert (NULL != krd.response_uncompressed); - setup_general_response_headers (ksh, - krd.response_uncompressed); - /* Information is always public, revalidate after 1 day */ - GNUNET_break (MHD_YES == - MHD_add_response_header (krd.response_uncompressed, - MHD_HTTP_HEADER_ETAG, - etag)); - /* Also compute compressed version of /keys response */ - comp = TALER_MHD_body_compress (&keys_jsonz, - &keys_jsonz_size); - krd.response_compressed - = MHD_create_response_from_buffer (keys_jsonz_size, - keys_jsonz, - MHD_RESPMEM_MUST_FREE); - GNUNET_assert (NULL != krd.response_compressed); - /* If the response is actually compressed, set the - respective header. */ - GNUNET_assert ( (MHD_YES != comp) || - (MHD_YES == - MHD_add_response_header (krd.response_compressed, - MHD_HTTP_HEADER_CONTENT_ENCODING, - "deflate")) ); - setup_general_response_headers (ksh, - krd.response_compressed); - /* Information is always public, revalidate after 1 day */ - GNUNET_break (MHD_YES == - MHD_add_response_header (krd.response_compressed, - MHD_HTTP_HEADER_ETAG, - etag)); - krd.etag = GNUNET_strdup (etag); - } - krd.cherry_pick_date = last_cherry_pick_date; - GNUNET_array_append (ksh->krd_array, - ksh->krd_array_length, - krd); - return GNUNET_OK; -} - - -/** - * Element in the `struct SignatureContext` array. - */ -struct SignatureElement -{ - - /** - * Offset of the denomination in the group array, - * for sorting (2nd rank, ascending). - */ - unsigned int offset; - - /** - * Offset of the group in the denominations array, - * for sorting (2nd rank, ascending). - */ - unsigned int group_offset; - - /** - * Pointer to actual master signature to hash over. - */ - struct TALER_MasterSignatureP master_sig; -}; - -/** - * Context for collecting the array of master signatures - * needed to verify the exchange_sig online signature. - */ -struct SignatureContext -{ - /** - * Array of signatures to hash over. - */ - struct SignatureElement *elements; - - /** - * Write offset in the @e elements array. - */ - unsigned int elements_pos; - - /** - * Allocated space for @e elements. - */ - unsigned int elements_size; -}; - - -/** - * Determine order to sort two elements by before - * we hash the master signatures. Used for - * sorting with qsort(). - * - * @param a pointer to a `struct SignatureElement` - * @param b pointer to a `struct SignatureElement` - * @return 0 if equal, -1 if a < b, 1 if a > b. - */ -static int -signature_context_sort_cb (const void *a, - const void *b) -{ - const struct SignatureElement *sa = a; - const struct SignatureElement *sb = b; - - if (sa->group_offset < sb->group_offset) - return -1; - if (sa->group_offset > sb->group_offset) - return 1; - if (sa->offset < sb->offset) - return -1; - if (sa->offset > sb->offset) - return 1; - /* We should never have two disjoint elements - with same time and offset */ - GNUNET_assert (sa == sb); - return 0; -} - - -/** - * Append a @a master_sig to the @a sig_ctx using the - * given attributes for (later) sorting. - * - * @param[in,out] sig_ctx signature context to update - * @param group_offset offset for the group - * @param offset offset for the entry - * @param master_sig master signature for the entry - */ -static void -append_signature (struct SignatureContext *sig_ctx, - unsigned int group_offset, - unsigned int offset, - const struct TALER_MasterSignatureP *master_sig) -{ - struct SignatureElement *element; - unsigned int new_size; - - if (sig_ctx->elements_pos == sig_ctx->elements_size) - { - if (0 == sig_ctx->elements_size) - new_size = 1024; - else - new_size = sig_ctx->elements_size * 2; - GNUNET_array_grow (sig_ctx->elements, - sig_ctx->elements_size, - new_size); - } - element = &sig_ctx->elements[sig_ctx->elements_pos++]; - element->offset = offset; - element->group_offset = group_offset; - element->master_sig = *master_sig; -} - - -/** - *GroupData is the value we store for each group meta-data */ -struct GroupData -{ - /** - * The json blob with the group meta-data and list of denominations - */ - json_t *json; - - /** - * List of denominations for the group, - * included in @e json, do not free separately! - */ - json_t *list; - - /** - * Offset of the group in the final array. - */ - unsigned int group_off; - -}; - - -/** - * Helper function called to clean up the group data - * in the denominations_by_group below. - * - * @param cls unused - * @param key unused - * @param value a `struct GroupData` to free - * @return #GNUNET_OK - */ -static int -free_group (void *cls, - const struct GNUNET_HashCode *key, - void *value) -{ - struct GroupData *gd = value; - - (void) cls; - (void) key; - GNUNET_free (gd); - return GNUNET_OK; -} - - -static void -compute_msig_hash (struct SignatureContext *sig_ctx, - struct GNUNET_HashCode *hc) -{ - struct GNUNET_HashContext *hash_context; - - hash_context = GNUNET_CRYPTO_hash_context_start (); - qsort (sig_ctx->elements, - sig_ctx->elements_pos, - sizeof (struct SignatureElement), - &signature_context_sort_cb); - for (unsigned int i = 0; i<sig_ctx->elements_pos; i++) - { - struct SignatureElement *element = &sig_ctx->elements[i]; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Adding %u,%u,%s\n", - element->group_offset, - element->offset, - TALER_B2S (&element->master_sig)); - GNUNET_CRYPTO_hash_context_read (hash_context, - &element->master_sig, - sizeof (element->master_sig)); - } - GNUNET_CRYPTO_hash_context_finish (hash_context, - hc); -} - - -/** - * Update the "/keys" responses in @a ksh, computing the detailed replies. - * - * This function is to recompute all (including cherry-picked) responses we - * might want to return, based on the state already in @a ksh. - * - * @param[in,out] ksh state handle to update - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -finish_keys_response (struct TEH_KeyStateHandle *ksh) -{ - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - json_t *recoup; - struct SignKeyCtx sctx = { - .min_sk_frequency = GNUNET_TIME_UNIT_FOREVER_REL - }; - json_t *grouped_denominations = NULL; - struct GNUNET_TIME_Timestamp last_cherry_pick_date; - struct GNUNET_CONTAINER_Heap *heap; - struct SignatureContext sig_ctx = { 0 }; - /* Remember if we have any denomination with age restriction */ - bool has_age_restricted_denomination = false; - struct WireStateHandle *wsh; - - wsh = get_wire_state (); - if (! wsh->ready) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - if (0 == - json_array_size (json_object_get (wsh->json_reply, - "accounts")) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No wire accounts available. Refusing to generate /keys response.\n"); - return GNUNET_NO; - } - sctx.signkeys = json_array (); - GNUNET_assert (NULL != sctx.signkeys); - recoup = json_array (); - GNUNET_assert (NULL != recoup); - grouped_denominations = json_array (); - GNUNET_assert (NULL != grouped_denominations); - - GNUNET_CONTAINER_multipeermap_iterate (ksh->signkey_map, - &add_sign_key_cb, - &sctx); - if (0 == json_array_size (sctx.signkeys)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No online signing keys available. Refusing to generate /keys response.\n"); - ret = GNUNET_NO; - goto CLEANUP; - } - heap = GNUNET_CONTAINER_heap_create (GNUNET_CONTAINER_HEAP_ORDER_MAX); - { - struct DenomKeyCtx dkc = { - .recoup = recoup, - .heap = heap, - .min_dk_frequency = GNUNET_TIME_UNIT_FOREVER_REL, - }; - - GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, - &add_denom_key_cb, - &dkc); - ksh->rekey_frequency - = GNUNET_TIME_relative_min (dkc.min_dk_frequency, - sctx.min_sk_frequency); - } - - last_cherry_pick_date = GNUNET_TIME_UNIT_ZERO_TS; - - { - struct TEH_DenominationKey *dk; - struct GNUNET_CONTAINER_MultiHashMap *denominations_by_group; - - denominations_by_group = - GNUNET_CONTAINER_multihashmap_create (1024, - GNUNET_NO /* NO, because keys are only on the stack */ - ); - /* heap = max heap, sorted by start time */ - while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap))) - { - if (GNUNET_TIME_timestamp_cmp (last_cherry_pick_date, - !=, - dk->meta.start) && - (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time)) ) - { - /* - * 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. - */ - struct GNUNET_HashCode hc; - - compute_msig_hash (&sig_ctx, - &hc); - if (GNUNET_OK != - create_krd (ksh, - &hc, - last_cherry_pick_date, - sctx.signkeys, - recoup, - grouped_denominations)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to generate key response data for %s\n", - GNUNET_TIME_timestamp2s (last_cherry_pick_date)); - /* drain heap before destroying it */ - while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap))) - /* intentionally empty */; - GNUNET_CONTAINER_heap_destroy (heap); - goto CLEANUP; - } - } - - last_cherry_pick_date = dk->meta.start; - /* - * Group the denominations by {cipher, value, fees, age_mask}. - * - * For each group we save the group meta-data and the list of - * denominations in this group as a json-blob in the multihashmap - * denominations_by_group. - */ - { - struct GroupData *group; - json_t *entry; - struct GNUNET_HashCode key; - struct TALER_DenominationGroup meta = { - .cipher = dk->denom_pub.bsign_pub_key->cipher, - .value = dk->meta.value, - .fees = dk->meta.fees, - .age_mask = dk->meta.age_mask, - }; - - /* Search the group/JSON-blob for the key */ - TALER_denomination_group_get_key (&meta, - &key); - group = GNUNET_CONTAINER_multihashmap_get ( - denominations_by_group, - &key); - if (NULL == group) - { - /* There is no group for this meta-data yet, so we create a new group */ - bool age_restricted = meta.age_mask.bits != 0; - const char *cipher; - - group = GNUNET_new (struct GroupData); - switch (meta.cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - cipher = age_restricted ? "RSA+age_restricted" : "RSA"; - break; - case GNUNET_CRYPTO_BSA_CS: - cipher = age_restricted ? "CS+age_restricted" : "CS"; - break; - default: - GNUNET_assert (false); - } - /* Create a new array for the denominations in this group */ - group->list = json_array (); - GNUNET_assert (NULL != group->list); - group->json = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("cipher", - cipher), - GNUNET_JSON_pack_array_steal ("denoms", - group->list), - TALER_JSON_PACK_DENOM_FEES ("fee", - &meta.fees), - TALER_JSON_pack_amount ("value", - &meta.value)); - GNUNET_assert (NULL != group->json); - if (age_restricted) - { - GNUNET_assert ( - 0 == - json_object_set_new (group->json, - "age_mask", - json_integer ( - meta.age_mask.bits))); - /* Remember that we have found at least _one_ age restricted denomination */ - has_age_restricted_denomination = true; - } - group->group_off - = json_array_size (grouped_denominations); - GNUNET_assert (0 == - json_array_append_new ( - grouped_denominations, - group->json)); - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap_put (denominations_by_group, - &key, - group, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - } - - /* Now that we have found/created the right group, add the - denomination to the list */ - { - struct HelperDenomination *hd; - struct GNUNET_JSON_PackSpec key_spec; - bool private_key_lost; - - hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, - &dk->h_denom_pub.hash); - private_key_lost - = (NULL == hd) || - GNUNET_TIME_absolute_is_past ( - GNUNET_TIME_absolute_add ( - hd->start_time.abs_time, - hd->validity_duration)); - switch (meta.cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - key_spec = - GNUNET_JSON_pack_rsa_public_key ( - "rsa_pub", - dk->denom_pub.bsign_pub_key->details.rsa_public_key); - break; - case GNUNET_CRYPTO_BSA_CS: - key_spec = - GNUNET_JSON_pack_data_varsize ( - "cs_pub", - &dk->denom_pub.bsign_pub_key->details.cs_public_key, - sizeof (dk->denom_pub.bsign_pub_key->details.cs_public_key)); - break; - default: - GNUNET_assert (false); - } - - entry = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("master_sig", - &dk->master_sig), - GNUNET_JSON_pack_allow_null ( - private_key_lost - ? GNUNET_JSON_pack_bool ("lost", - true) - : GNUNET_JSON_pack_string ("dummy", - NULL)), - GNUNET_JSON_pack_timestamp ("stamp_start", - dk->meta.start), - GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", - dk->meta.expire_withdraw), - GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", - dk->meta.expire_deposit), - GNUNET_JSON_pack_timestamp ("stamp_expire_legal", - dk->meta.expire_legal), - key_spec - ); - GNUNET_assert (NULL != entry); - } - - /* Build up the running hash of all master signatures of the - denominations */ - append_signature (&sig_ctx, - group->group_off, - (unsigned int) json_array_size (group->list), - &dk->master_sig); - /* Finally, add the denomination to the list of denominations in this - group */ - GNUNET_assert (json_is_array (group->list)); - GNUNET_assert (0 == - json_array_append_new (group->list, - entry)); - } - } /* loop over heap ends */ - - GNUNET_CONTAINER_multihashmap_iterate (denominations_by_group, - &free_group, - NULL); - GNUNET_CONTAINER_multihashmap_destroy (denominations_by_group); - } - GNUNET_CONTAINER_heap_destroy (heap); - - if (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time)) - { - struct GNUNET_HashCode hc; - - compute_msig_hash (&sig_ctx, - &hc); - if (GNUNET_OK != - create_krd (ksh, - &hc, - last_cherry_pick_date, - sctx.signkeys, - recoup, - grouped_denominations)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to generate key response data for %s\n", - 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 - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No denomination keys available. Refusing to generate /keys response.\n"); - } - ret = GNUNET_OK; - -CLEANUP: - GNUNET_array_grow (sig_ctx.elements, - sig_ctx.elements_size, - 0); - json_decref (grouped_denominations); - if (NULL != sctx.signkeys) - json_decref (sctx.signkeys); - json_decref (recoup); - return ret; -} - - -/** - * Called with information about global fees. - * - * @param cls `struct TEH_KeyStateHandle *` we are building - * @param fees the global fees we charge - * @param purse_timeout when do purses time out - * @param history_expiration how long are account histories preserved - * @param purse_account_limit how many purses are free per account - * @param start_date from when are these fees valid (start date) - * @param end_date until when are these fees valid (end date, exclusive) - * @param master_sig master key signature affirming that this is the correct - * fee (of purpose #TALER_SIGNATURE_MASTER_GLOBAL_FEES) - */ -static void -global_fee_info_cb ( - void *cls, - const struct TALER_GlobalFeeSet *fees, - struct GNUNET_TIME_Relative purse_timeout, - struct GNUNET_TIME_Relative history_expiration, - uint32_t purse_account_limit, - struct GNUNET_TIME_Timestamp start_date, - struct GNUNET_TIME_Timestamp end_date, - const struct TALER_MasterSignatureP *master_sig) -{ - struct TEH_KeyStateHandle *ksh = cls; - struct TEH_GlobalFee *gf; - - if (GNUNET_OK != - TALER_exchange_offline_global_fee_verify ( - start_date, - end_date, - fees, - purse_timeout, - history_expiration, - purse_account_limit, - &TEH_master_public_key, - master_sig)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database has global fee with invalid signature. Skipping entry. Did the exchange offline public key change?\n"); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found global fees with %u purses\n", - purse_account_limit); - gf = GNUNET_new (struct TEH_GlobalFee); - gf->start_date = start_date; - gf->end_date = end_date; - gf->fees = *fees; - gf->purse_timeout = purse_timeout; - gf->history_expiration = history_expiration; - gf->purse_account_limit = purse_account_limit; - gf->master_sig = *master_sig; - GNUNET_CONTAINER_DLL_insert (ksh->gf_head, - ksh->gf_tail, - gf); - GNUNET_assert ( - 0 == - json_array_append_new ( - ksh->global_fees, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ("start_date", - start_date), - GNUNET_JSON_pack_timestamp ("end_date", - end_date), - TALER_JSON_PACK_GLOBAL_FEES (fees), - GNUNET_JSON_pack_time_rel ("history_expiration", - history_expiration), - GNUNET_JSON_pack_time_rel ("purse_timeout", - purse_timeout), - GNUNET_JSON_pack_uint64 ("purse_account_limit", - purse_account_limit), - GNUNET_JSON_pack_data_auto ("master_sig", - master_sig)))); -} - - -/** - * Create a key state. - * - * @param management_only if we should NOT run 'finish_keys_response()' - * because we only need the state for the /management/keys API - * @return NULL on error (i.e. failed to access database) - */ -static struct TEH_KeyStateHandle * -build_key_state (bool management_only) -{ - struct TEH_KeyStateHandle *ksh; - enum GNUNET_DB_QueryStatus qs; - - ksh = GNUNET_new (struct TEH_KeyStateHandle); - ksh->signature_expires = GNUNET_TIME_UNIT_FOREVER_TS; - ksh->reload_time = GNUNET_TIME_timestamp_get (); - /* We must use the key_generation from when we STARTED the process! */ - ksh->key_generation = key_generation; - ksh->denomserial_map = GNUNET_CONTAINER_multihashmap32_create (1024); - ksh->denomkey_map = GNUNET_CONTAINER_multihashmap_create (1024, - true); - ksh->signkey_map = GNUNET_CONTAINER_multipeermap_create (32, - false /* MUST be false! */ - ); - ksh->auditors = json_array (); - GNUNET_assert (NULL != ksh->auditors); - /* NOTE: fetches master-signed signkeys, but ALSO those that were revoked! */ - GNUNET_break (GNUNET_OK == - TEH_plugin->preflight (TEH_plugin->cls)); - if (NULL != ksh->global_fees) - json_decref (ksh->global_fees); - ksh->global_fees = json_array (); - qs = TEH_plugin->get_global_fees (TEH_plugin->cls, - &global_fee_info_cb, - ksh); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Loading global fees from DB: %d\n", - qs); - if (qs < 0) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); - destroy_key_state (ksh); - return NULL; - } - qs = TEH_plugin->iterate_denominations (TEH_plugin->cls, - &denomination_info_cb, - ksh); - if (qs < 0) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); - destroy_key_state (ksh); - return NULL; - } - /* NOTE: ONLY fetches non-revoked AND master-signed signkeys! */ - qs = TEH_plugin->iterate_active_signkeys (TEH_plugin->cls, - &signkey_info_cb, - ksh); - if (qs < 0) - { - GNUNET_break (0); - destroy_key_state (ksh); - return NULL; - } - qs = TEH_plugin->iterate_auditor_denominations (TEH_plugin->cls, - &auditor_denom_cb, - ksh); - if (qs < 0) - { - GNUNET_break (0); - destroy_key_state (ksh); - return NULL; - } - qs = TEH_plugin->iterate_active_auditors (TEH_plugin->cls, - &auditor_info_cb, - ksh); - if (qs < 0) - { - GNUNET_break (0); - destroy_key_state (ksh); - return NULL; - } - - if (management_only) - { - ksh->management_only = true; - return ksh; - } - - if (GNUNET_OK != - finish_keys_response (ksh)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Could not finish /keys response (required data not configured yet)\n"); - destroy_key_state (ksh); - return NULL; - } - TEH_resume_keys_requests (false); - return ksh; -} - - -void -TEH_keys_update_states () -{ - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED), - }; - - TEH_plugin->event_notify (TEH_plugin->cls, - &es, - NULL, - 0); - key_generation++; - TEH_resume_keys_requests (false); -} - - -static struct TEH_KeyStateHandle * -keys_get_state (bool management_only) -{ - struct TEH_KeyStateHandle *old_ksh; - struct TEH_KeyStateHandle *ksh; - - old_ksh = key_state; - if (NULL == old_ksh) - { - ksh = build_key_state (management_only); - if (NULL == ksh) - return NULL; - key_state = ksh; - return ksh; - } - if ( (old_ksh->key_generation < key_generation) || - (GNUNET_TIME_absolute_is_past (old_ksh->signature_expires.abs_time)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Rebuilding /keys, generation upgrade from %llu to %llu\n", - (unsigned long long) old_ksh->key_generation, - (unsigned long long) key_generation); - ksh = build_key_state (management_only); - key_state = ksh; - destroy_key_state (old_ksh); - return ksh; - } - sync_key_helpers (&helpers); - return old_ksh; -} - - -struct TEH_KeyStateHandle * -TEH_keys_get_state_for_management_only (void) -{ - return keys_get_state (true); -} - - -struct TEH_KeyStateHandle * -TEH_keys_get_state (void) -{ - struct TEH_KeyStateHandle *ksh; - - ksh = keys_get_state (false); - if (NULL == ksh) - return NULL; - - if (ksh->management_only) - { - if (GNUNET_OK != - finish_keys_response (ksh)) - return NULL; - } - - return ksh; -} - - -const struct TEH_GlobalFee * -TEH_keys_global_fee_by_time ( - struct TEH_KeyStateHandle *ksh, - struct GNUNET_TIME_Timestamp ts) -{ - for (const struct TEH_GlobalFee *gf = ksh->gf_head; - NULL != gf; - gf = gf->next) - { - if (GNUNET_TIME_timestamp_cmp (ts, - >=, - gf->start_date) && - GNUNET_TIME_timestamp_cmp (ts, - <, - gf->end_date)) - return gf; - } - return NULL; -} - - -struct TEH_DenominationKey * -TEH_keys_denomination_by_hash ( - const struct TALER_DenominationHashP *h_denom_pub, - struct MHD_Connection *conn, - MHD_RESULT *mret) -{ - struct TEH_KeyStateHandle *ksh; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - *mret = TALER_MHD_reply_with_error (conn, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return NULL; - } - - return TEH_keys_denomination_by_hash_from_state (ksh, - h_denom_pub, - conn, - mret); -} - - -struct TEH_DenominationKey * -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) -{ - struct TEH_DenominationKey *dk; - - dk = GNUNET_CONTAINER_multihashmap_get (ksh->denomkey_map, - &h_denom_pub->hash); - if (NULL == dk) - { - if (NULL == conn) - return NULL; - *mret = TEH_RESPONSE_reply_unknown_denom_pub_hash (conn, - h_denom_pub); - return NULL; - } - return dk; -} - - -struct TEH_DenominationKey * -TEH_keys_denomination_by_serial_from_state ( - const struct TEH_KeyStateHandle *ksh, - uint64_t denom_serial) -{ - struct TEH_DenominationKey *dk; - uint32_t serial32 = (uint32_t) denom_serial; - - GNUNET_assert (denom_serial == (uint64_t) serial32); - dk = GNUNET_CONTAINER_multihashmap32_get (ksh->denomserial_map, - serial32); - return dk; -} - - -enum TALER_ErrorCode -TEH_keys_denomination_batch_sign ( - unsigned int csds_length, - const struct TEH_CoinSignData csds[static csds_length], - bool for_melt, - struct TALER_BlindedDenominationSignature bss[static csds_length]) -{ - struct TEH_KeyStateHandle *ksh; - struct HelperDenomination *hd; - struct TALER_CRYPTO_RsaSignRequest rsrs[csds_length]; - struct TALER_CRYPTO_CsSignRequest csrs[csds_length]; - struct TALER_BlindedDenominationSignature rs[csds_length]; - struct TALER_BlindedDenominationSignature cs[csds_length]; - unsigned int rsrs_pos = 0; - unsigned int csrs_pos = 0; - enum TALER_ErrorCode ec; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; - for (unsigned int i = 0; i<csds_length; i++) - { - const struct TALER_DenominationHashP *h_denom_pub = csds[i].h_denom_pub; - const struct TALER_BlindedPlanchet *bp = csds[i].bp; - - hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, - &h_denom_pub->hash); - if (NULL == hd) - return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; - if (bp->blinded_message->cipher != - hd->denom_pub.bsign_pub_key->cipher) - return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - switch (hd->denom_pub.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - rsrs[rsrs_pos].h_rsa = &hd->h_details.h_rsa; - rsrs[rsrs_pos].msg - = bp->blinded_message->details.rsa_blinded_message.blinded_msg; - rsrs[rsrs_pos].msg_size - = bp->blinded_message->details.rsa_blinded_message.blinded_msg_size; - rsrs_pos++; - break; - case GNUNET_CRYPTO_BSA_CS: - csrs[csrs_pos].h_cs = &hd->h_details.h_cs; - csrs[csrs_pos].blinded_planchet - = &bp->blinded_message->details.cs_blinded_message; - csrs_pos++; - break; - default: - return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - } - } - - if (0 != rsrs_pos) - { - memset (rs, - 0, - sizeof (rs)); - } - if (0 != csrs_pos) - { - memset (cs, - 0, - sizeof (cs)); - } - ec = TALER_EC_NONE; - if (0 != csrs_pos) - { - ec = TALER_CRYPTO_helper_cs_batch_sign ( - helpers.csdh, - csrs_pos, - csrs, - for_melt, - (0 == rsrs_pos) ? bss : cs); - if (TALER_EC_NONE != ec) - { - for (unsigned int i = 0; i<csrs_pos; i++) - TALER_blinded_denom_sig_free (&cs[i]); - return ec; - } - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_CS] += csrs_pos; - } - if (0 != rsrs_pos) - { - ec = TALER_CRYPTO_helper_rsa_batch_sign ( - helpers.rsadh, - rsrs_pos, - rsrs, - (0 == csrs_pos) ? bss : rs); - if (TALER_EC_NONE != ec) - { - for (unsigned int i = 0; i<csrs_pos; i++) - TALER_blinded_denom_sig_free (&cs[i]); - for (unsigned int i = 0; i<rsrs_pos; i++) - TALER_blinded_denom_sig_free (&rs[i]); - return ec; - } - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA] += rsrs_pos; - } - - if ( (0 != csrs_pos) && - (0 != rsrs_pos) ) - { - rsrs_pos = 0; - csrs_pos = 0; - for (unsigned int i = 0; i<csds_length; i++) - { - const struct TALER_BlindedPlanchet *bp = csds[i].bp; - - switch (bp->blinded_message->cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - bss[i] = rs[rsrs_pos++]; - break; - case GNUNET_CRYPTO_BSA_CS: - bss[i] = cs[csrs_pos++]; - break; - default: - GNUNET_assert (0); - } - } - } - return TALER_EC_NONE; -} - - -enum TALER_ErrorCode -TEH_keys_denomination_cs_r_pub ( - const struct TEH_CsDeriveData *cdd, - bool for_melt, - struct GNUNET_CRYPTO_CSPublicRPairP *r_pub) -{ - const struct TALER_DenominationHashP *h_denom_pub = cdd->h_denom_pub; - const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdd->nonce; - struct TEH_KeyStateHandle *ksh; - struct HelperDenomination *hd; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; - } - hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, - &h_denom_pub->hash); - if (NULL == hd) - { - return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; - } - if (GNUNET_CRYPTO_BSA_CS != - hd->denom_pub.bsign_pub_key->cipher) - { - return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - } - - { - struct TALER_CRYPTO_CsDeriveRequest cdr = { - .h_cs = &hd->h_details.h_cs, - .nonce = nonce - }; - return TALER_CRYPTO_helper_cs_r_derive (helpers.csdh, - &cdr, - for_melt, - r_pub); - } -} - - -enum TALER_ErrorCode -TEH_keys_denomination_cs_batch_r_pub_simple ( - unsigned int cdds_length, - const struct TEH_CsDeriveData cdds[static cdds_length], - bool for_melt, - struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]) -{ - struct TEH_KeyStateHandle *ksh; - struct HelperDenomination *hd; - struct TALER_CRYPTO_CsDeriveRequest cdrs[cdds_length]; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; - } - for (unsigned int i = 0; i<cdds_length; i++) - { - const struct TALER_DenominationHashP *h_denom_pub = cdds[i].h_denom_pub; - const struct GNUNET_CRYPTO_CsSessionNonce *nonce = cdds[i].nonce; - - hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, - &h_denom_pub->hash); - if (NULL == hd) - { - return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; - } - if (GNUNET_CRYPTO_BSA_CS != - hd->denom_pub.bsign_pub_key->cipher) - { - return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - } - cdrs[i].h_cs = &hd->h_details.h_cs; - cdrs[i].nonce = nonce; - } - - return TALER_CRYPTO_helper_cs_r_batch_derive (helpers.csdh, - cdds_length, - cdrs, - for_melt, - r_pubs); -} - - -enum TALER_ErrorCode -TEH_keys_denomination_cs_batch_r_pub ( - const struct TEH_KeyStateHandle *ksh, - size_t num, - const struct TALER_DenominationHashP h_denom_pubs[static num], - const struct GNUNET_CRYPTO_CsSessionNonce nonces[static num], - bool for_melt, - struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static num], - size_t *err_idx) -{ - struct TALER_CRYPTO_CsDeriveRequest cdrs[num]; - - for (unsigned int i = 0; i<num; i++) - { - const struct TEH_DenominationKey *dk; - const struct HelperDenomination *hd; - - *err_idx = i; - - /* FIXME: right now we need both, - * TEH_DenominationKey and HelperDenomination, - * because only TEH_DenominationKey has .recoup_possible - */ - hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, - &h_denom_pubs[i].hash); - dk = TEH_keys_denomination_by_hash_from_state (ksh, - &h_denom_pubs[i], - NULL, - NULL); - if (NULL == hd) - return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; - - GNUNET_assert (NULL != dk); - - if (GNUNET_CRYPTO_BSA_CS != - hd->denom_pub.bsign_pub_key->cipher) - return TALER_EC_EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION - ; - - if (GNUNET_TIME_absolute_is_future (hd->start_time.abs_time)) - return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE; - - if (dk->recoup_possible) - return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED; - - if (GNUNET_TIME_relative_is_zero (hd->validity_duration)) - return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED; - - cdrs[i].h_cs = &hd->h_details.h_cs; - cdrs[i].nonce = &nonces[i]; - } - - return TALER_CRYPTO_helper_cs_r_batch_derive (helpers.csdh, - num, - cdrs, - for_melt, - r_pubs); -} - - -void -TEH_keys_denomination_revoke (const struct TALER_DenominationHashP *h_denom_pub) -{ - struct TEH_KeyStateHandle *ksh; - struct HelperDenomination *hd; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - GNUNET_break (0); - return; - } - hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, - &h_denom_pub->hash); - if (NULL == hd) - { - GNUNET_break (0); - return; - } - switch (hd->denom_pub.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_INVALID: - break; - case GNUNET_CRYPTO_BSA_RSA: - TALER_CRYPTO_helper_rsa_revoke (helpers.rsadh, - &hd->h_details.h_rsa); - TEH_keys_update_states (); - return; - case GNUNET_CRYPTO_BSA_CS: - TALER_CRYPTO_helper_cs_revoke (helpers.csdh, - &hd->h_details.h_cs); - TEH_keys_update_states (); - return; - } - GNUNET_break (0); - return; -} - - -enum TALER_ErrorCode -TEH_keys_exchange_sign_ ( - const struct GNUNET_CRYPTO_SignaturePurpose *purpose, - struct TALER_ExchangePublicKeyP *pub, - struct TALER_ExchangeSignatureP *sig) -{ - struct TEH_KeyStateHandle *ksh; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - /* This *can* happen if the exchange's crypto helper is not running - or had some bad error. */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Cannot sign request, no valid signing keys available.\n"); - return TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING; - } - return TEH_keys_exchange_sign2_ (ksh, - purpose, - pub, - sig); -} - - -enum TALER_ErrorCode -TEH_keys_exchange_sign2_ ( - void *cls, - const struct GNUNET_CRYPTO_SignaturePurpose *purpose, - struct TALER_ExchangePublicKeyP *pub, - struct TALER_ExchangeSignatureP *sig) -{ - struct TEH_KeyStateHandle *ksh = cls; - enum TALER_ErrorCode ec; - - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_EDDSA]++; - ec = TALER_CRYPTO_helper_esign_sign_ (helpers.esh, - purpose, - pub, - sig); - if (TALER_EC_NONE != ec) - return ec; - { - /* Here we check here that 'pub' is set to an exchange public key that is - actually signed by the master key! Otherwise, we happily continue to - use key material even if the offline signatures have not been made - yet! */ - struct GNUNET_PeerIdentity pid; - struct SigningKey *sk; - - pid.public_key = pub->eddsa_pub; - sk = GNUNET_CONTAINER_multipeermap_get (ksh->signkey_map, - &pid); - if (NULL == sk) - { - /* just to be safe, zero out the (valid) signature, as the key - should not or no longer be used */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot sign, offline key signatures are missing!\n"); - memset (sig, - 0, - sizeof (*sig)); - return TALER_EC_EXCHANGE_SIGNKEY_HELPER_OFFLINE_MISSING; - } - } - return ec; -} - - -void -TEH_keys_exchange_revoke (const struct TALER_ExchangePublicKeyP *exchange_pub) -{ - struct TEH_KeyStateHandle *ksh; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - GNUNET_break (0); - return; - } - TALER_CRYPTO_helper_esign_revoke (helpers.esh, - exchange_pub); - TEH_keys_update_states (); -} - - -/** - * Comparator used for a binary search by cherry_pick_date for @a key in the - * `struct KeysResponseData` array. See libc's qsort() and bsearch() functions. - * - * @param key pointer to a `struct GNUNET_TIME_Timestamp` - * @param value pointer to a `struct KeysResponseData` array entry - * @return 0 if time matches, -1 if key is smaller, 1 if key is larger - */ -static int -krd_search_comparator (const void *key, - const void *value) -{ - const struct GNUNET_TIME_Timestamp *kd = key; - const struct KeysResponseData *krd = value; - - if (GNUNET_TIME_timestamp_cmp (*kd, - >, - krd->cherry_pick_date)) - return -1; - if (GNUNET_TIME_timestamp_cmp (*kd, - <, - krd->cherry_pick_date)) - return 1; - return 0; -} - - -MHD_RESULT -TEH_keys_get_handler (struct TEH_RequestContext *rc, - const char *const args[]) -{ - struct GNUNET_TIME_Timestamp last_issue_date; - const char *etag; - - etag = MHD_lookup_connection_value (rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - (void) args; - { - const char *have_cherrypick; - - have_cherrypick = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, - "last_issue_date"); - if (NULL != have_cherrypick) - { - unsigned long long cherrypickn; - - if (1 != - sscanf (have_cherrypick, - "%llu", - &cherrypickn)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - have_cherrypick); - } - /* The following multiplication may overflow; but this should not really - be a problem, as giving back 'older' data than what the client asks for - (given that the client asks for data in the distant future) is not - problematic */ - last_issue_date = GNUNET_TIME_timestamp_from_s (cherrypickn); - } - else - { - last_issue_date = GNUNET_TIME_UNIT_ZERO_TS; - } - } - - { - struct TEH_KeyStateHandle *ksh; - const struct KeysResponseData *krd; - - ksh = TEH_keys_get_state (); - if ( (NULL == ksh) || - (0 == ksh->krd_array_length) ) - { - if ( ( (SKR_LIMIT <= skr_size) && - (GNUNET_TIME_relative_cmp ( - GNUNET_TIME_absolute_get_duration (rc->start_time), - >, - GNUNET_TIME_UNIT_SECONDS)) ) || - TEH_suicide) - { - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - TEH_suicide - ? "server terminating" - : "too many connections suspended waiting on /keys"); - } - return suspend_request (rc->connection); - } - krd = bsearch (&last_issue_date, - ksh->krd_array, - ksh->krd_array_length, - sizeof (struct KeysResponseData), - &krd_search_comparator); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Filtering /keys by cherry pick date %s found entry %u/%u\n", - GNUNET_TIME_timestamp2s (last_issue_date), - (unsigned int) (krd - ksh->krd_array), - ksh->krd_array_length); - if ( (NULL == krd) && - (ksh->krd_array_length > 0) ) - { - if (! GNUNET_TIME_absolute_is_zero (last_issue_date.abs_time)) - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client provided invalid cherry picking timestamp %s, returning full response\n", - GNUNET_TIME_timestamp2s (last_issue_date)); - krd = &ksh->krd_array[ksh->krd_array_length - 1]; - } - if (NULL == krd) - { - /* Likely keys not ready *yet*. - Wait until they are. */ - return suspend_request (rc->connection); - } - if ( (NULL != etag) && - (0 == strcmp (etag, - krd->etag)) ) - return TEH_RESPONSE_reply_not_modified (rc->connection, - krd->etag, - &setup_general_response_headers, - ksh); - - return MHD_queue_response ( - rc->connection, - MHD_HTTP_OK, - (TALER_MHD_CT_DEFLATE == - TALER_MHD_can_compress (rc->connection, - TALER_MHD_CT_DEFLATE)) - ? krd->response_compressed - : krd->response_uncompressed); - } -} - - -/** - * Load extension data, like fees, expiration times (!) and age restriction - * flags for the denomination type configured in section @a section_name. - * Before calling this function, the `start` and `validity_duration` times must - * already be initialized in @a meta. - * - * @param section_name section in the configuration to use - * @param[in,out] meta denomination type data to complete - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -load_extension_data (const char *section_name, - struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta) -{ - struct GNUNET_TIME_Relative deposit_duration; - struct GNUNET_TIME_Relative legal_duration; - - GNUNET_assert (! GNUNET_TIME_absolute_is_zero (meta->start.abs_time)); /* caller bug */ - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (TEH_cfg, - section_name, - "DURATION_SPEND", - &deposit_duration)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - section_name, - "DURATION_SPEND"); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (TEH_cfg, - section_name, - "DURATION_LEGAL", - &legal_duration)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - section_name, - "DURATION_LEGAL"); - return GNUNET_SYSERR; - } - meta->expire_deposit - = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (meta->expire_withdraw.abs_time, - deposit_duration)); - meta->expire_legal = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (meta->expire_deposit.abs_time, - legal_duration)); - if (GNUNET_OK != - TALER_config_get_amount (TEH_cfg, - section_name, - "VALUE", - &meta->value)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "Need amount for option `%s' in section `%s'\n", - "VALUE", - section_name); - return GNUNET_SYSERR; - } - if (0 != strcasecmp (TEH_currency, - meta->value.currency)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Need denomination value in section `%s' to use currency `%s'\n", - section_name, - TEH_currency); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - TALER_config_get_denom_fees (TEH_cfg, - TEH_currency, - section_name, - &meta->fees)) - return GNUNET_SYSERR; - meta->age_mask = load_age_mask (section_name); - return GNUNET_OK; -} - - -enum GNUNET_GenericReturnValue -TEH_keys_load_fees (struct TEH_KeyStateHandle *ksh, - const struct TALER_DenominationHashP *h_denom_pub, - struct TALER_DenominationPublicKey *denom_pub, - struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta) -{ - struct HelperDenomination *hd; - enum GNUNET_GenericReturnValue ok; - - hd = GNUNET_CONTAINER_multihashmap_get (helpers.denom_keys, - &h_denom_pub->hash); - if (NULL == hd) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Denomination %s not known\n", - GNUNET_h2s (&h_denom_pub->hash)); - return GNUNET_NO; - } - meta->start = hd->start_time; - meta->expire_withdraw = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (meta->start.abs_time, - hd->validity_duration)); - ok = load_extension_data (hd->section_name, - meta); - if (GNUNET_OK == ok) - { - GNUNET_assert (GNUNET_CRYPTO_BSA_INVALID != - hd->denom_pub.bsign_pub_key->cipher); - TALER_denom_pub_copy (denom_pub, - &hd->denom_pub); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No fees for `%s', voiding key\n", - hd->section_name); - memset (denom_pub, - 0, - sizeof (*denom_pub)); - } - return ok; -} - - -enum GNUNET_GenericReturnValue -TEH_keys_get_timing (const struct TALER_ExchangePublicKeyP *exchange_pub, - struct TALER_EXCHANGEDB_SignkeyMetaData *meta) -{ - struct TEH_KeyStateHandle *ksh; - struct HelperSignkey *hsk; - struct GNUNET_PeerIdentity pid; - - ksh = TEH_keys_get_state_for_management_only (); - if (NULL == ksh) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - - pid.public_key = exchange_pub->eddsa_pub; - hsk = GNUNET_CONTAINER_multipeermap_get (helpers.esign_keys, - &pid); - if (NULL == hsk) - { - GNUNET_break (0); - return GNUNET_NO; - } - meta->start = hsk->start_time; - - meta->expire_sign = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (meta->start.abs_time, - hsk->validity_duration)); - meta->expire_legal = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (meta->expire_sign.abs_time, - signkey_legal_duration)); - return GNUNET_OK; -} - - -/** - * Closure for #add_future_denomkey_cb and #add_future_signkey_cb. - */ -struct FutureBuilderContext -{ - /** - * Our key state. - */ - struct TEH_KeyStateHandle *ksh; - - /** - * Array of denomination keys. - */ - json_t *denoms; - - /** - * Array of signing keys. - */ - json_t *signkeys; - -}; - - -/** - * Function called on all of our current and future denomination keys - * known to the helper process. Filters out those that are current - * and adds the remaining denomination keys (with their configuration - * data) to the JSON array. - * - * @param cls the `struct FutureBuilderContext *` - * @param h_denom_pub hash of the denomination public key - * @param value a `struct HelperDenomination` - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -add_future_denomkey_cb (void *cls, - const struct GNUNET_HashCode *h_denom_pub, - void *value) -{ - struct FutureBuilderContext *fbc = cls; - struct HelperDenomination *hd = value; - struct TEH_DenominationKey *dk; - struct TALER_EXCHANGEDB_DenominationKeyMetaData meta = {0}; - - dk = GNUNET_CONTAINER_multihashmap_get (fbc->ksh->denomkey_map, - h_denom_pub); - if (NULL != dk) - return GNUNET_OK; /* skip: this key is already active! */ - if (GNUNET_TIME_relative_is_zero (hd->validity_duration)) - return GNUNET_OK; /* this key already expired! */ - meta.start = hd->start_time; - meta.expire_withdraw = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (meta.start.abs_time, - hd->validity_duration)); - if (GNUNET_OK != - load_extension_data (hd->section_name, - &meta)) - { - /* Woops, couldn't determine fee structure!? */ - return GNUNET_OK; - } - GNUNET_assert ( - 0 == - json_array_append_new ( - fbc->denoms, - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("value", - &meta.value), - GNUNET_JSON_pack_timestamp ("stamp_start", - meta.start), - GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", - meta.expire_withdraw), - GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", - meta.expire_deposit), - GNUNET_JSON_pack_timestamp ("stamp_expire_legal", - meta.expire_legal), - TALER_JSON_pack_denom_pub ("denom_pub", - &hd->denom_pub), - TALER_JSON_PACK_DENOM_FEES ("fee", - &meta.fees), - GNUNET_JSON_pack_data_auto ("denom_secmod_sig", - &hd->sm_sig), - GNUNET_JSON_pack_string ("section_name", - hd->section_name)))); - return GNUNET_OK; -} - - -/** - * Function called on all of our current and future exchange signing keys - * known to the helper process. Filters out those that are current - * and adds the remaining signing keys (with their configuration - * data) to the JSON array. - * - * @param cls the `struct FutureBuilderContext *` - * @param pid actually the exchange public key (type disguised) - * @param value a `struct HelperDenomination` - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -add_future_signkey_cb (void *cls, - const struct GNUNET_PeerIdentity *pid, - void *value) -{ - struct FutureBuilderContext *fbc = cls; - struct HelperSignkey *hsk = value; - struct SigningKey *sk; - struct GNUNET_TIME_Timestamp stamp_expire; - struct GNUNET_TIME_Timestamp legal_end; - - sk = GNUNET_CONTAINER_multipeermap_get (fbc->ksh->signkey_map, - pid); - if (NULL != sk) - return GNUNET_OK; /* skip: this key is already active */ - if (GNUNET_TIME_relative_is_zero (hsk->validity_duration)) - return GNUNET_OK; /* this key already expired! */ - stamp_expire = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (hsk->start_time.abs_time, - hsk->validity_duration)); - legal_end = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (stamp_expire.abs_time, - signkey_legal_duration)); - GNUNET_assert (0 == - json_array_append_new ( - fbc->signkeys, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("key", - &hsk->exchange_pub), - GNUNET_JSON_pack_timestamp ("stamp_start", - hsk->start_time), - GNUNET_JSON_pack_timestamp ("stamp_expire", - stamp_expire), - GNUNET_JSON_pack_timestamp ("stamp_end", - legal_end), - GNUNET_JSON_pack_data_auto ("signkey_secmod_sig", - &hsk->sm_sig)))); - return GNUNET_OK; -} - - -MHD_RESULT -TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, - struct MHD_Connection *connection) -{ - struct TEH_KeyStateHandle *ksh; - json_t *reply; - - (void) rh; - ksh = TEH_keys_get_state_for_management_only (); - if (NULL == ksh) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - "no key state"); - } - sync_key_helpers (&helpers); - if (NULL == ksh->management_keys_reply) - { - struct FutureBuilderContext fbc = { - .ksh = ksh, - .denoms = json_array (), - .signkeys = json_array () - }; - - GNUNET_assert (NULL != fbc.denoms); - GNUNET_assert (NULL != fbc.signkeys); - if ( (GNUNET_is_zero (&denom_rsa_sm_pub)) && - (GNUNET_is_zero (&denom_cs_sm_pub)) ) - { - /* Either IPC failed, or neither helper had any denominations configured. */ - json_decref (fbc.denoms); - json_decref (fbc.signkeys); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_GATEWAY, - TALER_EC_EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE, - NULL); - } - if (GNUNET_is_zero (&esign_sm_pub)) - { - json_decref (fbc.denoms); - json_decref (fbc.signkeys); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_GATEWAY, - TALER_EC_EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE, - NULL); - } - GNUNET_CONTAINER_multihashmap_iterate (helpers.denom_keys, - &add_future_denomkey_cb, - &fbc); - GNUNET_CONTAINER_multipeermap_iterate (helpers.esign_keys, - &add_future_signkey_cb, - &fbc); - reply = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("future_denoms", - fbc.denoms), - GNUNET_JSON_pack_array_steal ("future_signkeys", - fbc.signkeys), - GNUNET_JSON_pack_data_auto ("master_pub", - &TEH_master_public_key), - GNUNET_JSON_pack_data_auto ("denom_secmod_public_key", - &denom_rsa_sm_pub), - GNUNET_JSON_pack_data_auto ("denom_secmod_cs_public_key", - &denom_cs_sm_pub), - GNUNET_JSON_pack_data_auto ("signkey_secmod_public_key", - &esign_sm_pub)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning GET /management/keys response:\n"); - if (NULL == reply) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - NULL); - } - GNUNET_assert (NULL == ksh->management_keys_reply); - ksh->management_keys_reply = reply; - } - else - { - reply = ksh->management_keys_reply; - } - return TALER_MHD_reply_json (connection, - reply, - MHD_HTTP_OK); -} - - -/* end of taler-exchange-httpd_keys.c */ diff --git a/src/exchange/taler-exchange-httpd_keys.h b/src/exchange/taler-exchange-httpd_keys.h @@ -1,614 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_keys.h - * @brief management of our various keys - * @defgroup crypto Cryptographic routines - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_responses.h" - - -#ifndef TALER_EXCHANGE_HTTPD_KEYS_H -#define TALER_EXCHANGE_HTTPD_KEYS_H - -/** - * Signatures of an auditor over a denomination key of this exchange. - */ -struct TEH_AuditorSignature; - - -/** - * @brief All information about a denomination key (which is used to - * sign coins into existence). - */ -struct TEH_DenominationKey -{ - - /** - * Decoded denomination public key (the hash of it is in - * @e issue, but we sometimes need the full public key as well). - */ - struct TALER_DenominationPublicKey denom_pub; - - /** - * Hash code of the denomination public key. - */ - struct TALER_DenominationHashP h_denom_pub; - - /** - * Meta data about the type of the denomination, such as fees and validity - * periods. - */ - struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; - - /** - * The long-term offline master key's signature for this denomination. - * Signs over @e h_denom_pub and @e meta. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * We store the auditor signatures for this denomination in a DLL. - */ - struct TEH_AuditorSignature *as_head; - - /** - * We store the auditor signatures for this denomination in a DLL. - */ - struct TEH_AuditorSignature *as_tail; - - /** - * Set to 'true' if this denomination has been revoked and recoup is - * thus supported right now. - */ - bool recoup_possible; - -}; - - -/** - * Set of global fees (and options) for a time range. - */ -struct TEH_GlobalFee -{ - /** - * Kept in a DLL. - */ - struct TEH_GlobalFee *next; - - /** - * Kept in a DLL. - */ - struct TEH_GlobalFee *prev; - - /** - * Beginning of the validity period (inclusive). - */ - struct GNUNET_TIME_Timestamp start_date; - - /** - * End of the validity period (exclusive). - */ - struct GNUNET_TIME_Timestamp end_date; - - /** - * How long do unmerged purses stay around at most? - */ - struct GNUNET_TIME_Relative purse_timeout; - - /** - * What is the longest history we return? - */ - struct GNUNET_TIME_Relative history_expiration; - - /** - * Signature affirming these details. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Fee structure for operations that do not depend - * on a denomination or wire method. - */ - struct TALER_GlobalFeeSet fees; - - /** - * Number of free purses per account. - */ - uint32_t purse_account_limit; -}; - - -/** - * Snapshot of the (coin and signing) keys (including private keys) of - * the exchange. There can be multiple instances of this struct, as it is - * reference counted and only destroyed once the last user is done - * with it. The current instance is acquired using - * #TEH_KS_acquire(). Using this function increases the - * reference count. The contents of this structure (except for the - * reference counter) should be considered READ-ONLY until it is - * ultimately destroyed (as there can be many concurrent users). - */ -struct TEH_KeyStateHandle; - - -/** - * Run internal invariant checks. For debugging. - */ -void -TEH_check_invariants (void); - - -/** - * Look up wire fee structure by @a ts. - * - * @param ts timestamp to lookup wire fees at - * @param method wire method to lookup fees for - * @return the wire fee details, or - * NULL if none are configured for @a ts and @a method - */ -const struct TALER_WireFeeSet * -TEH_wire_fees_by_time ( - struct GNUNET_TIME_Timestamp ts, - const char *method); - - -/** - * Something changed in the database. Rebuild the wire replies. This function - * should be called if the exchange learns about a new signature from our - * master key. - * - * (We do not do so immediately, but merely signal to all threads that they - * need to rebuild their wire state upon the next call to - * #TEH_keys_get_state()). - */ -void -TEH_wire_update_state (void); - - -/** - * Return the current key state for this thread. Possibly re-builds the key - * state if we have reason to believe that something changed. - * - * The result is ONLY valid until the next call to - * #TEH_keys_denomination_by_hash() or #TEH_keys_get_state() - * or #TEH_keys_exchange_sign(). - * - * @return NULL on error - */ -struct TEH_KeyStateHandle * -TEH_keys_get_state (void); - -/** - * Obtain the key state if we should NOT run finish_keys_response() because we - * only need the state for the /management/keys API - */ -struct TEH_KeyStateHandle * -TEH_keys_get_state_for_management_only (void); - -/** - * Something changed in the database. Rebuild all key states. This function - * should be called if the exchange learns about a new signature from an - * auditor or our master key. - * - * (We do not do so immediately, but merely signal to all threads that they - * need to rebuild their key state upon the next call to - * #TEH_keys_get_state()). - */ -void -TEH_keys_update_states (void); - - -/** - * Look up global fee structure by @a ts. - * - * @param ksh key state state to look in - * @param ts timestamp to lookup global fees at - * @return the global fee details, or - * NULL if none are configured for @a ts - */ -const struct TEH_GlobalFee * -TEH_keys_global_fee_by_time ( - struct TEH_KeyStateHandle *ksh, - struct GNUNET_TIME_Timestamp ts); - - -/** - * Look up the issue for a denom public key. Note that the result - * must only be used in this thread and only until another key or - * key state is resolved. - * - * @param h_denom_pub hash of denomination public key - * @param[in,out] conn used to return status message if NULL is returned - * @param[out] mret set to the MHD status if NULL is returned - * @return the denomination key issue, - * or NULL if @a h_denom_pub could not be found - */ -struct TEH_DenominationKey * -TEH_keys_denomination_by_hash ( - const struct TALER_DenominationHashP *h_denom_pub, - struct MHD_Connection *conn, - MHD_RESULT *mret); - - -/** - * Look up the issue for a denom public key using a given @a ksh. This allows - * requesting multiple denominations with the same @a ksh which thus will - * remain valid until the next call to #TEH_keys_denomination_by_hash() or - * #TEH_keys_get_state() or #TEH_keys_exchange_sign(). - * - * @param ksh key state state to look in - * @param h_denom_pub hash of denomination public key - * @param[in,out] conn connection used to return status message if NULL is returned - * @param[out] mret set to the MHD status if NULL is returned - * @return the denomination key issue, - * or NULL if @a h_denom_pub could not be found - */ -struct TEH_DenominationKey * -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); - - -/** - * Look up the issue for a denom public key using a given @a ksh. This allows - * requesting multiple denominations with the same @a ksh which thus will - * remain valid until the next call to #TEH_keys_denomination_by_hash() or - * #TEH_keys_get_state() or #TEH_keys_exchange_sign(). - * - * @param ksh key state state to look in - * @param denom_serial serial ID of the denomination in the table - * @return the denomination key issue, - * or NULL if @a denom_serial could not be found - */ -struct TEH_DenominationKey * -TEH_keys_denomination_by_serial_from_state ( - const struct TEH_KeyStateHandle *ksh, - uint64_t denom_serial); - - -/** - * Information needed to create a blind signature. - */ -struct TEH_CoinSignData -{ - /** - * Hash of key to sign with. - */ - const struct TALER_DenominationHashP *h_denom_pub; - - /** - * Blinded planchet to sign over. - */ - const struct TALER_BlindedPlanchet *bp; -}; - - -/** - * Request to sign @a csds. - * - * @param csds array with data to blindly sign (and keys to sign with) - * @param csds_length length of @a csds array - * @param for_melt true if this is for a melt operation - * @param[out] bss array set to the blind signature on success; must be of length @a csds_length - * @return #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TEH_keys_denomination_batch_sign ( - unsigned int csds_length, - const struct TEH_CoinSignData csds[static csds_length], - bool for_melt, - struct TALER_BlindedDenominationSignature bss[static csds_length]); - - -/** - * Information needed to derive the CS r_pub. - */ -struct TEH_CsDeriveData -{ - /** - * Hash of key to sign with. - */ - const struct TALER_DenominationHashP *h_denom_pub; - - /** - * Nonce to use. - */ - const struct GNUNET_CRYPTO_CsSessionNonce *nonce; -}; - - -/** - * Request to derive CS @a r_pub using the denomination and nonce from @a cdd. - * - * @param cdd data to compute @a r_pub from - * @param for_melt true if this is for a melt operation - * @param[out] r_pub where to write the result - * @return #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TEH_keys_denomination_cs_r_pub ( - const struct TEH_CsDeriveData *cdd, - bool for_melt, - struct GNUNET_CRYPTO_CSPublicRPairP *r_pub); - - -/** - * Request to derive a bunch of CS @a r_pubs using the - * denominations and nonces from @a cdds. - * - * @param cdds array to compute @a r_pubs from - * @param cdds_length length of the @a cdds array - * @param for_melt true if this is for a melt operation - * @param[out] r_pubs array where to write the result; must be of length @a cdds_length - * @return #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TEH_keys_denomination_cs_batch_r_pub_simple ( - unsigned int cdds_length, - const struct TEH_CsDeriveData cdds[static cdds_length], - bool for_melt, - struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static cdds_length]); - - -/** - * Request to derive a bunch of CS @a r_pubs using the - * denominations and nonces from @a cdds. - * - * @param ksh keys state to load the keys from - * @param num number of input elements - * @param h_denom_pubs array @a num of hashes of keys to sign with - * @param nonces array @a num of nonces to use - * @param for_melt true if this is for a melt operation - * @param[out] r_pubs array where to write the result; must be of length @a num - * @param[out] err_idx in case of error, the index into @e cdds that caused it - * @return #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TEH_keys_denomination_cs_batch_r_pub ( - const struct TEH_KeyStateHandle *ksh, - size_t num, - const struct TALER_DenominationHashP h_denom_pubs[static num], - const struct GNUNET_CRYPTO_CsSessionNonce nonces[static num], - bool for_melt, - struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static num], - size_t *err_idx); - -/** - * Revoke the public key associated with @a h_denom_pub. - * This function should be called AFTER the database was - * updated, as it also triggers #TEH_keys_update_states(). - * - * Note that the actual revocation happens asynchronously and - * may thus fail silently. To verify that the revocation succeeded, - * clients must watch for the associated change to the key state. - * - * @param h_denom_pub hash of the public key to revoke - */ -void -TEH_keys_denomination_revoke ( - const struct TALER_DenominationHashP *h_denom_pub); - - -/** - * Fully clean up keys subsystem. - */ -void -TEH_keys_finished (void); - - -/** - * Resumes all suspended /keys requests, we may now have key material - * (or are shutting down). - * - * @param do_shutdown are we shutting down? - */ -void -TEH_resume_keys_requests (bool do_shutdown); - - -/** - * Sign the message in @a purpose with the exchange's signing key. - * - * The @a purpose data is the beginning of the data of which the signature is - * to be created. The `size` field in @a purpose must correctly indicate the - * number of bytes of the data structure, including its header. Use - * #TEH_keys_exchange_sign() instead of calling this function directly! - * - * @param purpose the message to sign - * @param[out] pub set to the current public signing key of the exchange - * @param[out] sig signature over purpose using current signing key - * @return #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TEH_keys_exchange_sign_ ( - const struct GNUNET_CRYPTO_SignaturePurpose *purpose, - struct TALER_ExchangePublicKeyP *pub, - struct TALER_ExchangeSignatureP *sig); - - -/** - * Sign the message in @a purpose with the exchange's signing key. - * - * The @a purpose data is the beginning of the data of which the signature is - * to be created. The `size` field in @a purpose must correctly indicate the - * number of bytes of the data structure, including its header. Use - * #TEH_keys_exchange_sign() instead of calling this function directly! - * - * @param cls key state state to look in - * @param purpose the message to sign - * @param[out] pub set to the current public signing key of the exchange - * @param[out] sig signature over purpose using current signing key - * @return #TALER_EC_NONE on success - */ -enum TALER_ErrorCode -TEH_keys_exchange_sign2_ ( - void *cls, - const struct GNUNET_CRYPTO_SignaturePurpose *purpose, - struct TALER_ExchangePublicKeyP *pub, - struct TALER_ExchangeSignatureP *sig); - - -/** - * @ingroup crypto - * @brief EdDSA sign a given block. - * - * The @a ps data must be a fixed-size struct for which the signature is to be - * created. The `size` field in @a ps->purpose must correctly indicate the - * number of bytes of the data structure, including its header. - * - * @param ps packed struct with what to sign, MUST begin with a purpose - * @param[out] pub where to store the public key to use for the signing - * @param[out] sig where to write the signature - * @return #TALER_EC_NONE on success - */ -#define TEH_keys_exchange_sign(ps,pub,sig) \ - ({ \ - /* check size is set correctly */ \ - GNUNET_assert (htonl ((ps)->purpose.size) == \ - sizeof (*ps)); \ - /* check 'ps' begins with the purpose */ \ - GNUNET_static_assert (((void*) (ps)) == \ - ((void*) &(ps)->purpose)); \ - TEH_keys_exchange_sign_ (&(ps)->purpose, \ - pub, \ - sig); \ - }) - - -/** - * @ingroup crypto - * @brief EdDSA sign a given block. - * - * The @a ps data must be a fixed-size struct for which the signature is to be - * created. The `size` field in @a ps->purpose must correctly indicate the - * number of bytes of the data structure, including its header. - * - * This allows requesting multiple denominations with the same @a ksh which - * thus will remain valid until the next call to - * #TEH_keys_denomination_by_hash() or #TEH_keys_get_state() or - * #TEH_keys_exchange_sign(). - * - * @param ksh key state to use - * @param ps packed struct with what to sign, MUST begin with a purpose - * @param[out] pub where to store the public key to use for the signing - * @param[out] sig where to write the signature - * @return #TALER_EC_NONE on success - */ -#define TEH_keys_exchange_sign2(ksh,ps,pub,sig) \ - ({ \ - /* check size is set correctly */ \ - GNUNET_assert (htonl ((ps)->purpose.size) == \ - sizeof (*ps)); \ - /* check 'ps' begins with the purpose */ \ - GNUNET_static_assert (((void*) (ps)) == \ - ((void*) &(ps)->purpose)); \ - TEH_keys_exchange_sign2_ (ksh, \ - &(ps)->purpose, \ - pub, \ - sig); \ - }) - - -/** - * Revoke the given exchange's signing key. - * This function should be called AFTER the database was - * updated, as it also triggers #TEH_keys_update_states(). - * - * Note that the actual revocation happens asynchronously and - * may thus fail silently. To verify that the revocation succeeded, - * clients must watch for the associated change to the key state. - * - * @param exchange_pub key to revoke - */ -void -TEH_keys_exchange_revoke (const struct TALER_ExchangePublicKeyP *exchange_pub); - - -/** - * Function to call to handle requests to "/keys" by sending - * back our current key material. - * - * @param rc request context - * @param args array of additional options (must be empty for this function) - * @return MHD result code - */ -MHD_RESULT -TEH_keys_get_handler (struct TEH_RequestContext *rc, - const char *const args[]); - - -/** - * Function to call to handle requests to "/management/keys" by sending - * back our future key material. - * - * @param rh context of the handler - * @param connection the MHD connection to handle - * @return MHD result code - */ -MHD_RESULT -TEH_keys_management_get_keys_handler (const struct TEH_RequestHandler *rh, - struct MHD_Connection *connection); - - -/** - * Load fees and expiration times (!) for the denomination type configured for - * the denomination matching @a h_denom_pub. - * - * @param ksh key state to load fees from - * @param h_denom_pub hash of the denomination public key - * to use to derive the section name of the configuration to use - * @param[out] denom_pub set to the denomination public key (to be freed by caller!) - * @param[out] meta denomination type data to complete - * @return #GNUNET_OK on success, - * #GNUNET_NO if @a h_denom_pub is not known - * #GNUNET_SYSERR on hard errors - */ -enum GNUNET_GenericReturnValue -TEH_keys_load_fees (struct TEH_KeyStateHandle *ksh, - const struct TALER_DenominationHashP *h_denom_pub, - struct TALER_DenominationPublicKey *denom_pub, - struct TALER_EXCHANGEDB_DenominationKeyMetaData *meta); - - -/** - * Load expiration times for the given onling signing key. - * - * @param exchange_pub the online signing key - * @param[out] meta set to meta data about the key - * @return #GNUNET_OK on success - */ -enum GNUNET_GenericReturnValue -TEH_keys_get_timing (const struct TALER_ExchangePublicKeyP *exchange_pub, - struct TALER_EXCHANGEDB_SignkeyMetaData *meta); - - -/** - * Initialize keys subsystem. - * - * @return #GNUNET_OK on success - */ -enum GNUNET_GenericReturnValue -TEH_keys_init (void); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_kyc-check.c b/src/exchange/taler-exchange-httpd_kyc-check.c @@ -1,506 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-check.c - * @brief Handle request for generic KYC check. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_kyc-check.h" -#include "taler-exchange-httpd_kyc-wallet.h" -#include "taler-exchange-httpd_responses.h" - -/** - * Reserve GET request that is long-polling. - */ -struct KycPoller -{ - /** - * Kept in a DLL. - */ - struct KycPoller *next; - - /** - * Kept in a DLL. - */ - struct KycPoller *prev; - - /** - * Connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Subscription for the database event we are - * waiting for. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * Account for which we perform the KYC check. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * When will this request time out? - */ - struct GNUNET_TIME_Absolute timeout; - - /** - * Signature by the account owner authorizing this - * operation. - */ - union TALER_AccountSignatureP account_sig; - - /** - * Public key from the account owner authorizing this - * operation. Optional, see @e have_pub. - */ - union TALER_AccountPublicKeyP account_pub; - - /** - * Generation of KYC rules already known to the client - * (when long-polling). Do not send these rules again. - */ - uint64_t min_rule; - - /** - * What are we long-polling for (if anything)? - */ - enum TALER_EXCHANGE_KycLongPollTarget lpt; - - /** - * True if we are still suspended. - */ - bool suspended; - - /** - * True if we have an @e account_pub. - */ - bool have_pub; - -}; - - -/** - * Head of list of requests in long polling. - */ -static struct KycPoller *kyp_head; - -/** - * Tail of list of requests in long polling. - */ -static struct KycPoller *kyp_tail; - - -void -TEH_kyc_check_cleanup () -{ - struct KycPoller *kyp; - - while (NULL != (kyp = kyp_head)) - { - GNUNET_CONTAINER_DLL_remove (kyp_head, - kyp_tail, - kyp); - if (kyp->suspended) - { - kyp->suspended = false; - MHD_resume_connection (kyp->connection); - } - } -} - - -/** - * Function called once a connection is done to - * clean up the `struct ReservePoller` state. - * - * @param rc context to clean up for - */ -static void -kyp_cleanup (struct TEH_RequestContext *rc) -{ - struct KycPoller *kyp = rc->rh_ctx; - - GNUNET_assert (! kyp->suspended); - if (NULL != kyp->eh) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Cancelling DB event listening (KYP cleanup)\n"); - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - kyp->eh); - kyp->eh = NULL; - } - GNUNET_free (kyp); -} - - -/** - * Function called on events received from Postgres. - * Wakes up long pollers. - * - * @param cls the `struct TEH_RequestContext *` - * @param extra additional event data provided - * @param extra_size number of bytes in @a extra - */ -static void -db_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - struct TEH_RequestContext *rc = cls; - struct KycPoller *kyp = rc->rh_ctx; - struct GNUNET_AsyncScopeSave old_scope; - - (void) extra; - (void) extra_size; - if (! kyp->suspended) - return; /* event triggered while main transaction - was still running, or got multiple wake-up events */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received KYC update event\n"); - kyp->suspended = false; - GNUNET_async_scope_enter (&rc->async_scope_id, - &old_scope); - TEH_check_invariants (); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming from long-polling on KYC status\n"); - GNUNET_CONTAINER_DLL_remove (kyp_head, - kyp_tail, - kyp); - MHD_resume_connection (kyp->connection); - TALER_MHD_daemon_trigger (); - TEH_check_invariants (); - GNUNET_async_scope_restore (&old_scope); -} - - -MHD_RESULT -TEH_handler_kyc_check ( - struct TEH_RequestContext *rc, - const char *const args[1]) -{ - struct KycPoller *kyp = rc->rh_ctx; - json_t *jrules = NULL; - json_t *jlimits = NULL; - union TALER_AccountPublicKeyP reserve_pub; - struct TALER_AccountAccessTokenP access_token; - bool aml_review; - bool kyc_required; - bool access_ok = false; - enum GNUNET_GenericReturnValue is_wallet; - uint64_t rule_gen = 0; - - if (NULL == kyp) - { - bool sig_required = true; - - kyp = GNUNET_new (struct KycPoller); - kyp->connection = rc->connection; - rc->rh_ctx = kyp; - rc->rh_cleaner = &kyp_cleanup; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &kyp->h_payto, - sizeof (kyp->h_payto))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PATH_SEGMENT_MALFORMED, - "h_payto"); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Checking KYC status for normalized payto hash %s\n", - args[0]); - TALER_MHD_parse_request_header_auto ( - rc->connection, - TALER_HTTP_HEADER_ACCOUNT_OWNER_SIGNATURE, - &kyp->account_sig, - sig_required); - TALER_MHD_parse_request_header_auto ( - rc->connection, - TALER_HTTP_HEADER_ACCOUNT_OWNER_PUBKEY, - &kyp->account_pub, - kyp->have_pub); - TALER_MHD_parse_request_timeout (rc->connection, - &kyp->timeout); - { - uint64_t num = 0; - int val; - - TALER_MHD_parse_request_number (rc->connection, - "lpt", - &num); - val = (int) num; - if ( (val < 0) || - (val > TALER_EXCHANGE_KLPT_MAX) ) - { - /* Protocol violation, but we can be graceful and - just ignore the long polling! */ - GNUNET_break_op (0); - val = TALER_EXCHANGE_KLPT_NONE; - } - kyp->lpt = (enum TALER_EXCHANGE_KycLongPollTarget) val; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Long polling for target %d with timeout %s\n", - val, - GNUNET_TIME_relative2s ( - GNUNET_TIME_absolute_get_remaining ( - kyp->timeout), - true)); - } - TALER_MHD_parse_request_number (rc->connection, - "min_rule", - &kyp->min_rule); - /* long polling needed? */ - if (GNUNET_TIME_absolute_is_future (kyp->timeout)) - { - struct TALER_KycCompletedEventP rep = { - .header.size = htons (sizeof (rep)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED), - .h_payto = kyp->h_payto - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting DB event listening for KYC COMPLETED\n"); - kyp->eh = TEH_plugin->event_listen ( - TEH_plugin->cls, - GNUNET_TIME_absolute_get_remaining (kyp->timeout), - &rep.header, - &db_event_cb, - rc); - } - } /* end initialization */ - - if (! TEH_enable_kyc) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC not enabled\n"); - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - - { - enum GNUNET_DB_QueryStatus qs; - bool do_suspend; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Looking up KYC requirements for account %s (%s)\n", - TALER_B2S (&kyp->h_payto), - kyp->have_pub ? "with account-pub" : "legacy wallet"); - qs = TEH_plugin->lookup_kyc_requirement_by_row ( - TEH_plugin->cls, - &kyp->h_payto, - kyp->have_pub, - &kyp->account_pub, - &is_wallet, - &reserve_pub.reserve_pub, - &access_token, - &rule_gen, - &jrules, - &aml_review, - &kyc_required); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_ec ( - rc->connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "lookup_kyc_requirement_by_row"); - } - - do_suspend = false; - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* account unknown */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Account unknown!\n"); - if ( (TALER_EXCHANGE_KLPT_NONE != kyp->lpt) && - (TALER_EXCHANGE_KLPT_KYC_OK != kyp->lpt) && - (GNUNET_TIME_absolute_is_future (kyp->timeout)) ) - { - do_suspend = true; - access_ok = true; /* for now */ - } - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found rule %llu (client wants > %llu)\n", - (unsigned long long) rule_gen, - (unsigned long long) kyp->min_rule); - access_ok = - ( (! GNUNET_is_zero (&kyp->account_pub) && - (GNUNET_OK == - TALER_account_kyc_auth_verify (&kyp->account_pub, - &kyp->account_sig)) ) || - (! GNUNET_is_zero (&reserve_pub) && - (GNUNET_OK == - TALER_account_kyc_auth_verify (&reserve_pub, - &kyp->account_sig)) ) ); - if (GNUNET_TIME_absolute_is_future (kyp->timeout) && - (rule_gen <= kyp->min_rule) ) - { - switch (kyp->lpt) - { - case TALER_EXCHANGE_KLPT_NONE: - /* If KLPT is not given, just go for rule generation */ - do_suspend = true; - break; - case TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER: - if (! access_ok) - do_suspend = true; - break; - case TALER_EXCHANGE_KLPT_INVESTIGATION_DONE: - if (! aml_review) - do_suspend = true; - break; - case TALER_EXCHANGE_KLPT_KYC_OK: - if (kyc_required) - do_suspend = true; - break; - } - } - } - - if (do_suspend && - (access_ok || - (TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER == kyp->lpt) ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending HTTP request on timeout (%s) for %d\n", - GNUNET_TIME_relative2s (GNUNET_TIME_absolute_get_remaining ( - kyp->timeout), - true), - (int) kyp->lpt); - GNUNET_assert (NULL != kyp->eh); - kyp->suspended = true; - GNUNET_CONTAINER_DLL_insert (kyp_head, - kyp_tail, - kyp); - MHD_suspend_connection (kyp->connection); - return MHD_YES; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning account unknown\n"); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN, - NULL); - } - } - - if (! access_ok) - { - json_decref (jrules); - jrules = NULL; - if (GNUNET_is_zero (&kyp->account_pub)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_KEY_UNKNOWN, - NULL); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning authorization failed\n"); - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED), - GNUNET_JSON_pack_data_auto ("expected_account_pub", - &kyp->account_pub)); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC rules apply to %s:\n", - (GNUNET_SYSERR == is_wallet) - ? "unknown account type" - : ( (GNUNET_YES == is_wallet) - ? "wallet" - : "account")); - if (NULL != jrules) - json_dumpf (jrules, - stderr, - JSON_INDENT (2)); - - jlimits = TALER_KYCLOGIC_rules_to_limits (jrules, - is_wallet); - if (NULL == jlimits) - { - GNUNET_break_op (0); - json_decref (jrules); - jrules = NULL; - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "/kyc-check: rules_to_limits failed"); - } - json_decref (jrules); - jrules = NULL; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC limits apply:\n"); - json_dumpf (jlimits, - stderr, - JSON_INDENT (2)); - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning KYC %s\n", - kyc_required ? "required" : "optional"); - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - kyc_required - ? MHD_HTTP_ACCEPTED - : MHD_HTTP_OK, - GNUNET_JSON_pack_bool ("aml_review", - aml_review), - GNUNET_JSON_pack_uint64 ("rule_gen", - rule_gen), - GNUNET_JSON_pack_data_auto ("access_token", - &access_token), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ("limits", - jlimits))); -} - - -/* end of taler-exchange-httpd_kyc-check.c */ diff --git a/src/exchange/taler-exchange-httpd_kyc-check.h b/src/exchange/taler-exchange-httpd_kyc-check.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-check.h - * @brief Handle /kyc-check requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_KYC_CHECK_H -#define TALER_EXCHANGE_HTTPD_KYC_CHECK_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/kyc-check" request. Checks the KYC - * status of the given account and returns it. - * - * @param rc details about the request to handle - * @param args one argument with the legitimization_uuid - * @return MHD result code - */ -MHD_RESULT -TEH_handler_kyc_check ( - struct TEH_RequestContext *rc, - const char *const args[1]); - - -/** - * Clean up long-polling KYC requests during shutdown. - */ -void -TEH_kyc_check_cleanup (void); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_kyc-info.c b/src/exchange/taler-exchange-httpd_kyc-info.c @@ -1,825 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-info.c - * @brief Handle request for generic KYC info. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_exchangedb_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_kyc-info.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_common_kyc.h" - - -/** - * Context for the GET /kyc-info request. - * - * Used for long-polling and other asynchronous waiting. - */ -struct KycPoller -{ - /** - * Kept in a DLL. - */ - struct KycPoller *next; - - /** - * Kept in a DLL. - */ - struct KycPoller *prev; - - /** - * Connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Subscription for the database event we are - * waiting for. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * Handle to async activity to get the latest legitimization - * rule set. - */ - struct TALER_EXCHANGEDB_RuleUpdater *ru; - - /** - * Current legitimization rule set, owned by callee. Will be NULL on error - * or for default rules. Will not contain skip rules and not be expired. - */ - struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs; - - /** - * Handle for async KYC processing. - */ - struct TEH_KycMeasureRunContext *kat; - - /** - * Response to return, NULL if none yet. - */ - struct MHD_Response *response; - - /** - * Set to access token for a KYC process by the account, - * if @e have_token is true. - */ - struct TALER_AccountAccessTokenP access_token; - - /** - * Payto hash of the account matching @a access_token. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * #MHD_HTTP_HEADER_IF_NONE_MATCH Etag value sent by the client. 0 for none - * (or malformed). - */ - uint64_t etag_outcome_in; - - /** - * #MHD_HTTP_HEADER_IF_NONE_MATCH Etag value sent by the client. 0 for none - * (or malformed). - */ - uint64_t etag_measure_in; - - /** - * When will this request time out? - */ - struct GNUNET_TIME_Absolute timeout; - - /** - * - */ - uint64_t legitimization_measure_last_row; - - /** - * Row in the legitimization outcomes table that @e lrs matches. - */ - uint64_t legitimization_outcome_last_row; - - /** - * HTTP status code to use with @e response. - */ - unsigned int response_code; - - /** - * #GNUNET_YES if our @e h_payto is for a wallet, - * #GNUNET_NO if it is for an account, - * #GNUNET_SYSERR if we do not know. - */ - enum GNUNET_GenericReturnValue is_wallet; - - /** - * True if we are still suspended. - */ - bool suspended; - -}; - - -/** - * Head of list of requests in long polling. - */ -static struct KycPoller *kyp_head; - -/** - * Tail of list of requests in long polling. - */ -static struct KycPoller *kyp_tail; - - -void -TEH_kyc_info_cleanup () -{ - struct KycPoller *kyp; - - while (NULL != (kyp = kyp_head)) - { - GNUNET_CONTAINER_DLL_remove (kyp_head, - kyp_tail, - kyp); - if (kyp->suspended) - { - kyp->suspended = false; - MHD_resume_connection (kyp->connection); - } - } -} - - -/** - * Function called once a connection is done to - * clean up the `struct ReservePoller` state. - * - * @param rc context to clean up for - */ -static void -kyp_cleanup (struct TEH_RequestContext *rc) -{ - struct KycPoller *kyp = rc->rh_ctx; - - GNUNET_assert (! kyp->suspended); - if (NULL != kyp->eh) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Cancelling DB event listening\n"); - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - kyp->eh); - kyp->eh = NULL; - } - if (NULL != kyp->ru) - { - TALER_EXCHANGEDB_update_rules_cancel (kyp->ru); - kyp->ru = NULL; - } - if (NULL != kyp->response) - { - MHD_destroy_response (kyp->response); - kyp->response = NULL; - } - if (NULL != kyp->lrs) - { - TALER_KYCLOGIC_rules_free (kyp->lrs); - kyp->lrs = NULL; - } - if (NULL != kyp->kat) - { - TEH_kyc_run_measure_cancel (kyp->kat); - kyp->kat = NULL; - } - GNUNET_free (kyp); -} - - -/** - * Function called on events received from Postgres. - * Wakes up long pollers. - * - * @param cls the `struct TEH_RequestContext *` - * @param extra additional event data provided - * @param extra_size number of bytes in @a extra - */ -static void -db_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - struct TEH_RequestContext *rc = cls; - struct KycPoller *kyp = rc->rh_ctx; - struct GNUNET_AsyncScopeSave old_scope; - - (void) extra; - (void) extra_size; - if (! kyp->suspended) - return; /* event triggered while main transaction - was still running, or got multiple wake-up events */ - GNUNET_async_scope_enter (&rc->async_scope_id, - &old_scope); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming from long-polling on KYC status\n"); - GNUNET_CONTAINER_DLL_remove (kyp_head, - kyp_tail, - kyp); - kyp->suspended = false; - MHD_resume_connection (kyp->connection); - TALER_MHD_daemon_trigger (); - GNUNET_async_scope_restore (&old_scope); -} - - -/** - * Add the headers we want to set for every response. - * - * @param[in,out] response the response to modify - */ -static void -add_nocache_header (struct MHD_Response *response) -{ - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_CACHE_CONTROL, - "no-cache")); -} - - -/** - * Resume processing the @a kyp request with the @a response. - * - * @param kyp request to resume and respond to - * @param http_status HTTP status for @a response - * @param response HTTP response to return - */ -static void -resume_with_response (struct KycPoller *kyp, - unsigned int http_status, - struct MHD_Response *response) -{ - kyp->response_code = http_status; - kyp->response = response; - GNUNET_CONTAINER_DLL_remove (kyp_head, - kyp_tail, - kyp); - kyp->suspended = false; - MHD_resume_connection (kyp->connection); - TALER_MHD_daemon_trigger (); -} - - -/** - * Function called after a measure has been run. - * - * @param kyp request to fail with the error code - * @param ec error code or 0 on success - * @param hint detail error message or NULL on success / no info - */ -static void -fail_with_ec ( - struct KycPoller *kyp, - enum TALER_ErrorCode ec, - const char *hint) -{ - resume_with_response (kyp, - TALER_ErrorCode_get_http_status (ec), - TALER_MHD_make_error (ec, - hint)); -} - - -/** - * Generate a reply with the KycProcessClientInformation from - * the LegitimizationMeasures. - * - * @param[in,out] kyp request to reply on - * @param legitimization_measure_row_id part of etag to set for the response - * @param legitimization_outcome_row_id part of etag to set for the response - * @param jmeasures a `LegitimizationMeasures` object to encode - * @param jvoluntary array of voluntary measures to encode, can be NULL - */ -static void -resume_with_reply (struct KycPoller *kyp, - uint64_t legitimization_measure_row_id, - uint64_t legitimization_outcome_row_id, - const json_t *jmeasures, - const json_t *jvoluntary) -{ - const json_t *measures; /* array of MeasureInformation */ - bool is_and_combinator = false; - bool verboten; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("is_and_combinator", - &is_and_combinator), - NULL), - GNUNET_JSON_spec_bool ("verboten", - &verboten), - GNUNET_JSON_spec_array_const ("measures", - &measures), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; - const char *ename; - unsigned int eline; - json_t *kris; - size_t i; - json_t *mi; /* a MeasureInformation object */ - - ret = GNUNET_JSON_parse (jmeasures, - spec, - &ename, - &eline); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - fail_with_ec (kyp, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - ename); - return; - } - kris = json_array (); - GNUNET_assert (NULL != kris); - json_array_foreach ((json_t *) measures, i, mi) - { - const char *check_name; - const json_t *context = NULL; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("check_name", - &check_name), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("context", - &context), - NULL), - GNUNET_JSON_spec_end () - }; - json_t *kri; - - ret = GNUNET_JSON_parse (mi, - ispec, - &ename, - &eline); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - json_decref (kris); - fail_with_ec ( - kyp, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - ename); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found required check `%s'\n", - check_name); - if (NULL != context) - { - json_dumpf (context, - stderr, - JSON_INDENT (2)); - fprintf (stderr, - "\n"); - } - /* Check if requirement is a duplicate, and in that case do - not return it */ - { - bool duplicate = false; - - for (size_t off = 0; off < i; off++) - { - json_t *have = json_array_get (measures, - off); - if ( (1 == - json_equal (json_object_get (have, - "check_name"), - json_object_get (mi, - "check_name")) ) && - (1 == - json_equal (json_object_get (have, - "check_name"), - json_object_get (mi, - "check_name")) ) && - (1 == - json_equal (json_object_get (have, - "check_name"), - json_object_get (mi, - "check_name")) ) ) - { - /* Duplicate requirement, do not return again */ - duplicate = true; - break; - } - } - if (duplicate) - { - continue; - } - } - kri = TALER_KYCLOGIC_measure_to_requirement ( - check_name, - context, - &kyp->access_token, - i, - legitimization_measure_row_id); - if (NULL == kri) - { - GNUNET_break (0); - json_decref (kris); - fail_with_ec ( - kyp, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "could not convert measure to requirement"); - return; - } - GNUNET_assert (0 == - json_array_append_new (kris, - kri)); - } - - { - char etags[128]; - struct MHD_Response *resp; - - GNUNET_snprintf (etags, - sizeof (etags), - "\"%llu-%llu\"", - (unsigned long long) legitimization_measure_row_id, - (unsigned long long) legitimization_outcome_row_id); - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("requirements", - kris), - GNUNET_JSON_pack_bool ("is_and_combinator", - is_and_combinator), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_incref ( - "voluntary_measures", - (json_t *) jvoluntary))); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - etags)); - add_nocache_header (resp); - resume_with_response (kyp, - MHD_HTTP_OK, - resp); - } -} - - -/** - * Function called with the current rule set. - * - * @param cls closure with a `struct KycPoller *` - * @param rur includes legitimziation rule set that applies to the account - * (owned by callee, callee must free the lrs!) - */ -static void -current_rules_cb ( - void *cls, - struct TALER_EXCHANGEDB_RuleUpdaterResult *rur) -{ - struct KycPoller *kyp = cls; - enum GNUNET_DB_QueryStatus qs; - uint64_t legitimization_measure_last_row; - json_t *jmeasures; - json_t *vmeasures; - - kyp->ru = NULL; - if (TALER_EC_NONE != rur->ec) - { - /* Rollback should not be needed, just to be sure */ - TEH_plugin->rollback (TEH_plugin->cls); - fail_with_ec (kyp, - rur->ec, - rur->hint); - return; - } - GNUNET_assert (NULL == kyp->lrs); - kyp->lrs - = rur->lrs; - kyp->legitimization_outcome_last_row - = rur->legitimization_outcome_last_row; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "LRS is for the account uses %s\n", - NULL == kyp->lrs - ? "default rules" - : "custom rules"); - - /* Check if there is an unfinished legitimization measure */ - qs = TEH_plugin->lookup_kyc_status_by_token ( - TEH_plugin->cls, - &kyp->access_token, - &legitimization_measure_last_row, - &jmeasures); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "lookup_kyc_status_by_token returned %d\n", - (int) qs); - if (qs < 0) - { - GNUNET_break (0); - TEH_plugin->rollback (TEH_plugin->cls); - fail_with_ec ( - kyp, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_kyc_status_by_token"); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - jmeasures - = TALER_KYCLOGIC_zero_measures (kyp->lrs, - kyp->is_wallet); - if (NULL == jmeasures) - { - qs = TEH_plugin->commit (TEH_plugin->cls); - if (qs < 0) - { - TEH_plugin->rollback (TEH_plugin->cls); - fail_with_ec ( - kyp, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "kyc-info"); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No KYC requirement open\n"); - resume_with_response (kyp, - MHD_HTTP_OK, - TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ( - "voluntary_measures", - TALER_KYCLOGIC_voluntary_measures ( - kyp->lrs)) - ))); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Making applicable zero-measures for %s under current rules active\n", - (GNUNET_SYSERR == kyp->is_wallet) - ? "unknown account type" - : ( (GNUNET_YES == kyp->is_wallet) - ? "wallets" - : "accounts")); - json_dumpf (jmeasures, - stderr, - JSON_INDENT (2)); - qs = TEH_plugin->insert_active_legitimization_measure ( - TEH_plugin->cls, - &kyp->access_token, - jmeasures, - &legitimization_measure_last_row); - if (qs < 0) - { - GNUNET_break (0); - TEH_plugin->rollback (TEH_plugin->cls); - fail_with_ec (kyp, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_active_legitimization_measure"); - return; - } - } - if ( (legitimization_measure_last_row == kyp->etag_measure_in) && - (kyp->legitimization_outcome_last_row == kyp->etag_outcome_in) && - GNUNET_TIME_absolute_is_future (kyp->timeout) ) - { - /* Note: in practice this commit should do nothing, but we cannot - trust that the client provided correct etags, and so we must - commit anyway just in case the client lied about the etags. */ - qs = TEH_plugin->commit (TEH_plugin->cls); - if (qs < 0) - { - TEH_plugin->rollback (TEH_plugin->cls); - fail_with_ec ( - kyp, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "kyc-info"); - return; - } - if (NULL != kyp->lrs) - { - TALER_KYCLOGIC_rules_free (kyp->lrs); - kyp->lrs = NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending HTTP request on timeout (%s)\n", - GNUNET_TIME_relative2s ( - GNUNET_TIME_absolute_get_remaining ( - kyp->timeout), - true)); - GNUNET_assert (NULL != kyp->eh); - GNUNET_break (kyp->suspended); - return; - } - qs = TEH_plugin->commit (TEH_plugin->cls); - if (qs < 0) - { - TEH_plugin->rollback (TEH_plugin->cls); - fail_with_ec ( - kyp, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "kyc-info"); - return; - } - if ( (legitimization_measure_last_row == - kyp->etag_measure_in) && - (kyp->legitimization_outcome_last_row == - kyp->etag_outcome_in) ) - { - char etags[128]; - struct MHD_Response *resp; - - GNUNET_snprintf (etags, - sizeof (etags), - "\"%llu-%llu\"", - (unsigned long long) legitimization_measure_last_row, - (unsigned long long) kyp->legitimization_outcome_last_row); - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - add_nocache_header (resp); - TALER_MHD_add_global_headers (resp, - false); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - etags)); - resume_with_response (kyp, - MHD_HTTP_NOT_MODIFIED, - resp); - json_decref (jmeasures); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Generating success reply to kyc-info query\n"); - vmeasures = TALER_KYCLOGIC_voluntary_measures (kyp->lrs); - resume_with_reply (kyp, - legitimization_measure_last_row, - kyp->legitimization_outcome_last_row, - jmeasures, - vmeasures); - json_decref (vmeasures); - json_decref (jmeasures); -} - - -MHD_RESULT -TEH_handler_kyc_info ( - struct TEH_RequestContext *rc, - const char *const args[1]) -{ - struct KycPoller *kyp = rc->rh_ctx; - enum GNUNET_DB_QueryStatus qs; - - if (NULL == kyp) - { - bool bis_wallet; - - kyp = GNUNET_new (struct KycPoller); - kyp->connection = rc->connection; - rc->rh_ctx = kyp; - rc->rh_cleaner = &kyp_cleanup; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data ( - args[0], - strlen (args[0]), - &kyp->access_token, - sizeof (kyp->access_token))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "access token"); - } - TALER_MHD_parse_request_timeout (rc->connection, - &kyp->timeout); - - /* Get etag */ - { - const char *etags; - - etags = MHD_lookup_connection_value ( - rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if (NULL != etags) - { - char dummy; - unsigned long long ev1; - unsigned long long ev2; - - if (2 != sscanf (etags, - "\"%llu-%llu\"%c", - &ev1, - &ev2, - &dummy)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client send malformed `%s' header `%s'\n", - MHD_HTTP_HEADER_IF_NONE_MATCH, - etags); - } - else - { - kyp->etag_measure_in = (uint64_t) ev1; - kyp->etag_outcome_in = (uint64_t) ev2; - } - } - } /* etag */ - - /* Check access token */ - kyp->is_wallet = GNUNET_SYSERR; - qs = TEH_plugin->lookup_h_payto_by_access_token ( - TEH_plugin->cls, - &kyp->access_token, - &kyp->h_payto, - &bis_wallet); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_ec ( - rc->connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_h_payto_by_access_token"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_break_op (0); - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_INFO_AUTHORIZATION_FAILED)); - } - kyp->is_wallet = (bis_wallet) ? GNUNET_YES : GNUNET_NO; - - if (GNUNET_TIME_absolute_is_future (kyp->timeout)) - { - struct TALER_KycCompletedEventP rep = { - .header.size = htons (sizeof (rep)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED), - .h_payto = kyp->h_payto - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting DB event listening\n"); - kyp->eh = TEH_plugin->event_listen ( - TEH_plugin->cls, - GNUNET_TIME_absolute_get_remaining (kyp->timeout), - &rep.header, - &db_event_cb, - rc); - } - } /* end of one-time initialization */ - - if (NULL != kyp->response) - { - return MHD_queue_response (rc->connection, - kyp->response_code, - kyp->response); - } - - kyp->ru = TALER_EXCHANGEDB_update_rules (TEH_plugin, - &TEH_attribute_key, - &kyp->h_payto, - kyp->is_wallet, - &current_rules_cb, - kyp); - kyp->suspended = true; - GNUNET_CONTAINER_DLL_insert (kyp_head, - kyp_tail, - kyp); - MHD_suspend_connection (rc->connection); - return MHD_YES; - -} - - -/* end of taler-exchange-httpd_kyc-info.c */ diff --git a/src/exchange/taler-exchange-httpd_kyc-info.h b/src/exchange/taler-exchange-httpd_kyc-info.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-info.h - * @brief Handle /kyc-info requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_KYC_INFO_H -#define TALER_EXCHANGE_HTTPD_KYC_INFO_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Shutdown kyc-info subsystem. Resumes all suspended long-polling clients - * and cleans up data structures. - */ -void -TEH_kyc_info_cleanup (void); - - -/** - * Handle a "/kyc-info" request. - * - * @param rc request to handle - * @param args one argument with the legitimization_uuid - * @return MHD result code - */ -MHD_RESULT -TEH_handler_kyc_info ( - struct TEH_RequestContext *rc, - const char *const args[1]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_kyc-proof.c b/src/exchange/taler-exchange-httpd_kyc-proof.c @@ -1,641 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-proof.c - * @brief Handle request for proof for KYC check. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_attributes.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_templating_lib.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_kyc-proof.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Context for the proof. - */ -struct KycProofContext -{ - - /** - * Kept in a DLL while suspended. - */ - struct KycProofContext *next; - - /** - * Kept in a DLL while suspended. - */ - struct KycProofContext *prev; - - /** - * Details about the connection we are processing. - */ - struct TEH_RequestContext *rc; - - /** - * Proof logic to run. - */ - struct TALER_KYCLOGIC_Plugin *logic; - - /** - * Configuration for @a logic. - */ - struct TALER_KYCLOGIC_ProviderDetails *pd; - - /** - * Asynchronous operation with the proof system. - */ - struct TALER_KYCLOGIC_ProofHandle *ph; - - /** - * KYC AML trigger operation. - */ - struct TEH_KycMeasureRunContext *kat; - - /** - * Process information about the user for the plugin from the database, can - * be NULL. - */ - char *provider_user_id; - - /** - * Process information about the legitimization process for the plugin from the - * database, can be NULL. - */ - char *provider_legitimization_id; - - /** - * Hash of payment target URI this is about. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * Final HTTP response to return. - */ - struct MHD_Response *response; - - /** - * Final HTTP response code to return. - */ - unsigned int response_code; - - /** - * HTTP response from the KYC provider plugin. - */ - struct MHD_Response *proof_response; - - /** - * HTTP response code from the KYC provider plugin. - */ - unsigned int proof_response_code; - - /** - * Provider configuration section name of the logic we are running. - */ - const char *provider_name; - - /** - * Row in the database for this legitimization operation. - */ - uint64_t process_row; - - /** - * True if we are suspended, - */ - bool suspended; - - /** - * True if @e h_payto is for a wallet. - */ - bool is_wallet; - -}; - - -/** - * Contexts are kept in a DLL while suspended. - */ -static struct KycProofContext *kpc_head; - -/** - * Contexts are kept in a DLL while suspended. - */ -static struct KycProofContext *kpc_tail; - - -/** - * Generate HTML error for @a connection using @a template. - * - * @param connection HTTP client connection - * @param template template to expand - * @param[in,out] http_status HTTP status of the response - * @param ec Taler error code to return - * @param message extended message to return - * @return MHD response object - */ -static struct MHD_Response * -make_html_error (struct MHD_Connection *connection, - const char *template, - unsigned int *http_status, - enum TALER_ErrorCode ec, - const char *message) -{ - struct MHD_Response *response = NULL; - json_t *body; - enum GNUNET_GenericReturnValue ret; - - body = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("message", - message)), - TALER_JSON_pack_ec ( - ec)); - ret = TALER_TEMPLATING_build (connection, - http_status, - template, - NULL, - NULL, - body, - &response); - GNUNET_break (GNUNET_SYSERR != ret); - if (GNUNET_SYSERR != ret) - GNUNET_break (MHD_NO != - MHD_add_response_header (response, - MHD_HTTP_HEADER_CONTENT_TYPE, - "text/html")); - - json_decref (body); - return response; -} - - -/** - * Resume processing the @a kpc request. - * - * @param kpc request to resume - */ -static void -kpc_resume (struct KycProofContext *kpc) -{ - GNUNET_assert (GNUNET_YES == kpc->suspended); - kpc->suspended = false; - GNUNET_CONTAINER_DLL_remove (kpc_head, - kpc_tail, - kpc); - MHD_resume_connection (kpc->rc->connection); - TALER_MHD_daemon_trigger (); -} - - -void -TEH_kyc_proof_cleanup (void) -{ - struct KycProofContext *kpc; - - while (NULL != (kpc = kpc_head)) - { - if (NULL != kpc->ph) - { - kpc->logic->proof_cancel (kpc->ph); - kpc->ph = NULL; - } - kpc_resume (kpc); - } -} - - -/** - * Function called after the KYC-AML trigger is done. - * - * @param cls closure - * @param ec error code or 0 on success - * @param detail error message or NULL on success / no info - */ -static void -proof_finish ( - void *cls, - enum TALER_ErrorCode ec, - const char *detail) -{ - struct KycProofContext *kpc = cls; - - kpc->kat = NULL; - if (TALER_EC_NONE != ec) - { - kpc->response_code = TALER_ErrorCode_get_http_status (ec); - GNUNET_break (5 != kpc->response_code / 100); - GNUNET_assert (kpc->response_code != UINT_MAX); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Templating error response for %d and HTTP status %u (%s)\n", - (int) ec, - kpc->response_code, - detail); - kpc->response = make_html_error ( - kpc->rc->connection, - "kyc-proof-internal-error", - &kpc->response_code, - ec, - detail); - } - else - { - GNUNET_assert (NULL != kpc->proof_response); - kpc->response_code = kpc->proof_response_code; - kpc->response = kpc->proof_response; - kpc->proof_response = NULL; - kpc->proof_response_code = 0; - } - GNUNET_assert (NULL != kpc->response); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming with response %p and status %u\n", - kpc->response, - kpc->response_code); - kpc_resume (kpc); -} - - -/** - * Respond with an HTML message on the given @a rc. - * - * @param[in,out] rc request to respond to - * @param http_status HTTP status code to use - * @param template template to fill in - * @param ec error code to use for the template - * @param message additional message to return - * @return MHD result code - */ -static MHD_RESULT -respond_html_ec (struct TEH_RequestContext *rc, - unsigned int http_status, - const char *template, - enum TALER_ErrorCode ec, - const char *message) -{ - struct MHD_Response *response; - MHD_RESULT res; - - response = make_html_error (rc->connection, - template, - &http_status, - ec, - message); - res = MHD_queue_response (rc->connection, - http_status, - response); - MHD_destroy_response (response); - return res; -} - - -/** - * Function called with the result of a proof check operation. - * - * Note that the "decref" for the @a response - * will be done by the callee and MUST NOT be done by the plugin. - * - * @param cls closure - * @param status KYC status - * @param provider_name name of the provider - * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown - * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown - * @param expiration until when is the KYC check valid - * @param attributes user attributes returned by the provider - * @param http_status HTTP status code of @a response - * @param[in] response to return to the HTTP client - */ -static void -proof_cb ( - void *cls, - enum TALER_KYCLOGIC_KycStatus status, - const char *provider_name, - const char *provider_user_id, - const char *provider_legitimization_id, - struct GNUNET_TIME_Absolute expiration, - const json_t *attributes, - unsigned int http_status, - struct MHD_Response *response) -{ - struct KycProofContext *kpc = cls; - struct TEH_RequestContext *rc = kpc->rc; - struct GNUNET_AsyncScopeSave old_scope; - enum GNUNET_DB_QueryStatus qs; - - kpc->ph = NULL; - kpc->proof_response = response; - kpc->proof_response_code = http_status; - GNUNET_async_scope_enter (&rc->async_scope_id, - &old_scope); - switch (status) - { - case TALER_KYCLOGIC_STATUS_SUCCESS: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC process #%llu succeeded with KYC provider\n", - (unsigned long long) kpc->process_row); - qs = TEH_kyc_store_attributes ( - kpc->process_row, - &kpc->h_payto, - provider_name, - provider_user_id, - provider_legitimization_id, - expiration, - attributes); - if (0 >= qs) - { - GNUNET_break (0); - proof_finish (kpc, - TALER_EC_GENERIC_DB_STORE_FAILED, - "kyc_store_attributes"); - break; - } - - kpc->kat = TEH_kyc_run_measure_for_attributes ( - &rc->async_scope_id, - kpc->process_row, - &kpc->h_payto, - kpc->is_wallet, - &proof_finish, - kpc); - if (NULL == kpc->kat) - { - GNUNET_break_op (0); - proof_finish (kpc, - TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, - NULL); - } - break; - case TALER_KYCLOGIC_STATUS_FAILED: - case TALER_KYCLOGIC_STATUS_PROVIDER_FAILED: - case TALER_KYCLOGIC_STATUS_USER_ABORTED: - case TALER_KYCLOGIC_STATUS_ABORTED: - GNUNET_assert (NULL == kpc->kat); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC process %s/%s (Row #%llu) failed: %d\n", - provider_user_id, - provider_legitimization_id, - (unsigned long long) kpc->process_row, - status); - if (5 == http_status / 100) - { - char *msg; - - /* OAuth2 server had a problem, do NOT log this as a KYC failure */ - GNUNET_break (0); - GNUNET_asprintf (&msg, - "Failure by KYC provider (HTTP status %u)\n", - http_status); - proof_finish ( - kpc, - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, - msg); - GNUNET_free (msg); - break; - } - if (! TEH_kyc_failed ( - kpc->process_row, - &kpc->h_payto, - kpc->provider_name, - provider_user_id, - provider_legitimization_id, - TALER_KYCLOGIC_status2s (status), - TALER_EC_EXCHANGE_GENERIC_KYC_FAILED)) - { - GNUNET_break (0); - proof_finish ( - kpc, - TALER_EC_GENERIC_DB_STORE_FAILED, - "TEH_kyc_failed"); - break; - } - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "KYC process #%llu failed with status %d\n", - (unsigned long long) kpc->process_row, - status); - proof_finish (kpc, - TALER_EC_NONE, - NULL); - break; - default: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC status of %s/%s (Row #%llu) is %d\n", - provider_user_id, - provider_legitimization_id, - (unsigned long long) kpc->process_row, - (int) status); - break; - } - GNUNET_async_scope_restore (&old_scope); -} - - -/** - * Function called to clean up a context. - * - * @param rc request context - */ -static void -clean_kpc (struct TEH_RequestContext *rc) -{ - struct KycProofContext *kpc = rc->rh_ctx; - - if (NULL != kpc->ph) - { - kpc->logic->proof_cancel (kpc->ph); - kpc->ph = NULL; - } - if (NULL != kpc->kat) - { - TEH_kyc_run_measure_cancel (kpc->kat); - kpc->kat = NULL; - } - if (NULL != kpc->response) - { - MHD_destroy_response (kpc->response); - kpc->response = NULL; - } - if (NULL != kpc->proof_response) - { - MHD_destroy_response (kpc->proof_response); - kpc->proof_response = NULL; - } - GNUNET_free (kpc->provider_user_id); - GNUNET_free (kpc->provider_legitimization_id); - GNUNET_free (kpc); -} - - -MHD_RESULT -TEH_handler_kyc_proof ( - struct TEH_RequestContext *rc, - const char *const args[1]) -{ - struct KycProofContext *kpc = rc->rh_ctx; - const char *provider_name_or_logic = args[0]; - - if (NULL == kpc) - { - /* first time */ - if (NULL == provider_name_or_logic) - { - GNUNET_break_op (0); - return respond_html_ec ( - rc, - MHD_HTTP_NOT_FOUND, - "kyc-proof-endpoint-unknown", - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - "'/kyc-proof/$PROVIDER_NAME?state=$H_PAYTO' required"); - } - kpc = GNUNET_new (struct KycProofContext); - kpc->rc = rc; - rc->rh_ctx = kpc; - rc->rh_cleaner = &clean_kpc; - TALER_MHD_parse_request_arg_auto_t (rc->connection, - "state", - &kpc->h_payto); - if (GNUNET_OK != - TALER_KYCLOGIC_lookup_logic ( - provider_name_or_logic, - &kpc->logic, - &kpc->pd, - &kpc->provider_name)) - { - GNUNET_break_op (0); - return respond_html_ec ( - rc, - MHD_HTTP_NOT_FOUND, - "kyc-proof-target-unknown", - TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, - provider_name_or_logic); - } - if (NULL != kpc->provider_name) - { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Absolute expiration; - - if (0 != strcmp (provider_name_or_logic, - kpc->provider_name)) - { - GNUNET_break_op (0); - return respond_html_ec ( - rc, - MHD_HTTP_BAD_REQUEST, - "kyc-proof-bad-request", - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "PROVIDER_NAME"); - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Looking for KYC process at %s\n", - kpc->provider_name); - qs = TEH_plugin->lookup_kyc_process_by_account ( - TEH_plugin->cls, - kpc->provider_name, - &kpc->h_payto, - &kpc->process_row, - &expiration, - &kpc->provider_user_id, - &kpc->provider_legitimization_id, - &kpc->is_wallet); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return respond_html_ec ( - rc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "kyc-proof-internal-error", - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_kyc_process_by_account"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break_op (0); - return respond_html_ec ( - rc, - MHD_HTTP_NOT_FOUND, - "kyc-proof-target-unknown", - TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, - kpc->provider_name); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - if (GNUNET_TIME_absolute_is_future (expiration)) - { - /* KYC not required */ - return respond_html_ec ( - rc, - MHD_HTTP_OK, - "kyc-proof-already-done", - TALER_EC_NONE, - NULL); - } - } - kpc->ph = kpc->logic->proof ( - kpc->logic->cls, - kpc->pd, - rc->connection, - &kpc->h_payto, - kpc->process_row, - kpc->provider_user_id, - kpc->provider_legitimization_id, - &proof_cb, - kpc); - if (NULL == kpc->ph) - { - GNUNET_break (0); - return respond_html_ec ( - rc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "kyc-proof-internal-error", - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "could not start proof with KYC logic"); - } - - - kpc->suspended = true; - GNUNET_CONTAINER_DLL_insert (kpc_head, - kpc_tail, - kpc); - MHD_suspend_connection (rc->connection); - return MHD_YES; - } - - if (NULL == kpc->response) - { - GNUNET_break (0); - return respond_html_ec ( - rc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "kyc-proof-internal-error", - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "handler resumed without response"); - } - - /* return response from KYC logic */ - return MHD_queue_response (rc->connection, - kpc->response_code, - kpc->response); -} - - -/* end of taler-exchange-httpd_kyc-proof.c */ diff --git a/src/exchange/taler-exchange-httpd_kyc-proof.h b/src/exchange/taler-exchange-httpd_kyc-proof.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-proof.h - * @brief Handle /kyc-proof requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_KYC_PROOF_H -#define TALER_EXCHANGE_HTTPD_KYC_PROOF_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Shutdown kyc-proof subsystem. Resumes all suspended long-polling clients - * and cleans up data structures. - */ -void -TEH_kyc_proof_cleanup (void); - - -/** - * Handle a "/kyc-proof" request. - * - * @param rc request to handle - * @param args one argument with the legitimization_uuid - * @return MHD result code - */ -MHD_RESULT -TEH_handler_kyc_proof ( - struct TEH_RequestContext *rc, - const char *const args[1]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_kyc-start.c b/src/exchange/taler-exchange-httpd_kyc-start.c @@ -1,537 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-start.c - * @brief Handle request for starting a KYC process with an external provider. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_kyc-start.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * POST request in asynchronous processing. - */ -struct KycPoller -{ - - /** - * Access token for the KYC data of the account. - */ - struct TALER_AccountAccessTokenP access_token; - - /** - * Authorization hash for the selected measure. - */ - struct TALER_KycMeasureAuthorizationHashP shv; - - /** - * Hash of the payto:// URI we are starting to the KYC for. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * Kept in a DLL. - */ - struct KycPoller *next; - - /** - * Kept in a DLL. - */ - struct KycPoller *prev; - - /** - * Connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Logic for @e ih - */ - struct TALER_KYCLOGIC_Plugin *ih_logic; - - /** - * Handle to asynchronously running KYC initiation - * request. - */ - struct TALER_KYCLOGIC_InitiateHandle *ih; - - /** - * Set of applicable KYC measures. - */ - json_t *jmeasures; - - /** - * Where to redirect the user to start the KYC process. - */ - char *redirect_url; - - /** - * Set to the name of the KYC provider. - */ - const char *provider_name; - - /** - * Set to error details, on error (@ec not TALER_EC_NONE). - */ - char *hint; - - /** - * Row of the requirement being started. - */ - unsigned long long legitimization_measure_serial_id; - - /** - * Row of KYC process being initiated. - */ - uint64_t process_row; - - /** - * Index of the measure this upload is for. - */ - unsigned int measure_index; - - /** - * Set to error encountered with KYC logic, if any. - */ - enum TALER_ErrorCode ec; - - /** - * True if we are still suspended. - */ - bool suspended; - - /** - * True if @e h_payto is for a wallet - */ - bool is_wallet; - -}; - - -/** - * Head of list of requests in asynchronous processing. - */ -static struct KycPoller *kyp_head; - -/** - * Tail of list of requests in asynchronous processing. - */ -static struct KycPoller *kyp_tail; - - -void -TEH_kyc_start_cleanup () -{ - struct KycPoller *kyp; - - while (NULL != (kyp = kyp_head)) - { - GNUNET_CONTAINER_DLL_remove (kyp_head, - kyp_tail, - kyp); - if (NULL != kyp->ih) - { - kyp->ih_logic->initiate_cancel (kyp->ih); - kyp->ih = NULL; - } - if (kyp->suspended) - { - kyp->suspended = false; - MHD_resume_connection (kyp->connection); - } - } -} - - -/** - * Function called once a connection is done to - * clean up the `struct ReservePoller` state. - * - * @param rc context to clean up for - */ -static void -kyp_cleanup (struct TEH_RequestContext *rc) -{ - struct KycPoller *kyp = rc->rh_ctx; - - GNUNET_assert (! kyp->suspended); - if (NULL != kyp->ih) - { - kyp->ih_logic->initiate_cancel (kyp->ih); - kyp->ih = NULL; - } - GNUNET_free (kyp->redirect_url); - GNUNET_free (kyp->hint); - json_decref (kyp->jmeasures); - GNUNET_free (kyp); -} - - -/** - * Function called with the result of a KYC initiation - * operation. - * - * @param cls closure with our `struct KycPoller *` - * @param ec #TALER_EC_NONE on success - * @param redirect_url set to where to redirect the user on success, NULL on failure - * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown - * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown - * @param error_msg_hint set to additional details to return to user, NULL on success - */ -static void -initiate_cb ( - void *cls, - enum TALER_ErrorCode ec, - const char *redirect_url, - const char *provider_user_id, - const char *provider_legitimization_id, - const char *error_msg_hint) -{ - struct KycPoller *kyp = cls; - enum GNUNET_DB_QueryStatus qs; - - kyp->ih = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC initiation `%s' completed with ec=%d (%s)\n", - provider_legitimization_id, - ec, - (TALER_EC_NONE == ec) - ? redirect_url - : error_msg_hint); - kyp->ec = ec; - if (TALER_EC_NONE == ec) - { - kyp->redirect_url = GNUNET_strdup (redirect_url); - } - else - { - kyp->hint = GNUNET_strdup (error_msg_hint); - } - qs = TEH_plugin->update_kyc_process_by_row ( - TEH_plugin->cls, - kyp->process_row, - kyp->provider_name, - &kyp->h_payto, - provider_user_id, - provider_legitimization_id, - redirect_url, - GNUNET_TIME_UNIT_ZERO_ABS, - ec, - error_msg_hint, - TALER_EC_NONE != ec); - if (qs <= 0) - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "KYC requirement update failed for %s with status %d at %s:%u\n", - TALER_B2S (&kyp->h_payto), - qs, - __FILE__, - __LINE__); - GNUNET_assert (kyp->suspended); - kyp->suspended = false; - GNUNET_CONTAINER_DLL_remove (kyp_head, - kyp_tail, - kyp); - MHD_resume_connection (kyp->connection); - TALER_MHD_daemon_trigger (); -} - - -MHD_RESULT -TEH_handler_kyc_start ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[1]) -{ - struct KycPoller *kyp = rc->rh_ctx; - - (void) root; - if (NULL == kyp) - { - const char *id = args[0]; - enum GNUNET_DB_QueryStatus qs; - const struct TALER_KYCLOGIC_KycProvider *provider; - struct TALER_KYCLOGIC_ProviderDetails *pd; - bool is_finished; - const json_t *context; - - kyp = GNUNET_new (struct KycPoller); - kyp->connection = rc->connection; - rc->rh_ctx = kyp; - rc->rh_cleaner = &kyp_cleanup; - - { - char dummy; - const char *slash; - - slash = strchr (id, '-'); - if (NULL == slash) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - rc->url); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (id, - slash - id, - &kyp->shv, - sizeof (kyp->shv))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Authorization hash in ID is malformed"); - } - if (2 != - sscanf (slash + 1, - "%u-%llu%c", - &kyp->measure_index, - &kyp->legitimization_measure_serial_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "ID is malformed"); - } - } - qs = TEH_plugin->lookup_pending_legitimization ( - TEH_plugin->cls, - kyp->legitimization_measure_serial_id, - &kyp->access_token, - &kyp->h_payto, - &kyp->jmeasures, - &is_finished, - &kyp->is_wallet); - if (qs < 0) - { - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); - return TALER_MHD_reply_with_ec ( - rc->connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_pending_legitimization"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - rc->url); - } - if (is_finished) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED, - rc->url); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found pending legitimization %llu with measures\n", - (unsigned long long) kyp->legitimization_measure_serial_id); - json_dumpf (kyp->jmeasures, - stderr, - JSON_INDENT (2)); - fprintf (stderr, - "\n"); - - { - struct TALER_KycMeasureAuthorizationHashP shv2; - - TALER_kyc_measure_authorization_hash ( - &kyp->access_token, - kyp->legitimization_measure_serial_id, - kyp->measure_index, - &shv2); - if (0 != - GNUNET_memcmp (&kyp->shv, - &shv2)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - rc->url); - } - } - - { - const char *check_name; - const char *prog_name; - - kyp->ec = TALER_KYCLOGIC_select_measure ( - kyp->jmeasures, - kyp->measure_index, - &check_name, - &prog_name, - &context); - if (TALER_EC_NONE != kyp->ec) - { - /* return EC in next call to this function */ - GNUNET_break_op (0); - kyp->hint - = GNUNET_strdup ("TALER_KYCLOGIC_select_measure"); - return MHD_YES; - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Selected measure is %llu with check `%s'\n", - (unsigned long long) kyp->measure_index, - check_name); - if (NULL != context) - { - json_dumpf (context, - stderr, - JSON_INDENT (2)); - fprintf (stderr, - "\n"); - } - - provider = TALER_KYCLOGIC_check_to_provider ( - check_name); - if (NULL == provider) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_KYC_INVALID_LOGIC_TO_CHECK, - check_name); - } - } - - TALER_KYCLOGIC_provider_to_logic ( - provider, - &kyp->ih_logic, - &pd, - &kyp->provider_name); - - /* FIXME-#9419: the next two DB interactions should be ONE - transaction */ - /* Check if we already initiated this process */ - qs = TEH_plugin->get_pending_kyc_requirement_process ( - TEH_plugin->cls, - &kyp->h_payto, - kyp->provider_name, - &kyp->redirect_url); - if (qs < 0) - { - GNUNET_break (0); - /* Simple query, never should be a soft error. */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_pending_kyc_requirement_process"); - } - if (NULL != kyp->redirect_url) - return MHD_YES; /* success, return the redirect URL - (in next call to this function) */ - - /* set up new requirement process */ - qs = TEH_plugin->insert_kyc_requirement_process ( - TEH_plugin->cls, - &kyp->h_payto, - kyp->measure_index, - kyp->legitimization_measure_serial_id, - kyp->provider_name, - NULL, /* provider_account_id */ - NULL, /* provider_legitimziation_id */ - &kyp->process_row); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_kyc_requirement_process"); - } - - kyp->ih = kyp->ih_logic->initiate ( - kyp->ih_logic->cls, - pd, - &kyp->h_payto, - kyp->process_row, - context, - &initiate_cb, - kyp); - if (NULL == kyp->ih) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG, - "initiate KYC process"); - } - kyp->suspended = true; - GNUNET_CONTAINER_DLL_insert (kyp_head, - kyp_tail, - kyp); - MHD_suspend_connection (kyp->connection); - return MHD_YES; - } - - if ( (TALER_EC_NONE != kyp->ec) || - (NULL == kyp->redirect_url) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "KYC process failed to start with error %d (%s)\n", - (int) kyp->ec, - kyp->hint); - if (TALER_EC_NONE == kyp->ec) - { - GNUNET_break (0); - kyp->ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - } - return TALER_MHD_reply_with_ec (rc->connection, - kyp->ec, - kyp->hint); - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("redirect_url", - kyp->redirect_url)); -} - - -/* end of taler-exchange-httpd_kyc-start.c */ diff --git a/src/exchange/taler-exchange-httpd_kyc-start.h b/src/exchange/taler-exchange-httpd_kyc-start.h @@ -1,50 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-start.h - * @brief Handle /kyc-start requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_KYC_START_H -#define TALER_EXCHANGE_HTTPD_KYC_START_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Resume suspended requests for /kyc-start. - */ -void -TEH_kyc_start_cleanup (void); - - -/** - * Handle a "/kyc-start" request. - * - * @param rc request to handle - * @param root uploaded JSON data (empty by current API) - * @param args array with the ID - * @return MHD result code - */ -MHD_RESULT -TEH_handler_kyc_start ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[1]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_kyc-upload.c b/src/exchange/taler-exchange-httpd_kyc-upload.c @@ -1,567 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-upload.c - * @brief Handle /kyc-upload/$ID request - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_kyc-upload.h" - -#define MAX_RETRIES 3 - -/** - * Context used for processing the KYC upload req - */ -struct UploadContext -{ - - /** - * Kept in a DLL. - */ - struct UploadContext *next; - - /** - * Kept in a DLL. - */ - struct UploadContext *prev; - - /** - * Access token for the KYC data of the account. - */ - struct TALER_AccountAccessTokenP access_token; - - /** - * Index of the measure this upload is for. - */ - unsigned int measure_index; - - /** - * HTTP status code to use with @e response. - */ - unsigned int response_code; - - /** - * Index in the legitimization measures table this ID - * refers to. - */ - unsigned long long legitimization_measure_serial_id; - - /** - * Response to return, NULL if none yet. - */ - struct MHD_Response *response; - - /** - * Request we are processing. - */ - struct TEH_RequestContext *rc; - - /** - * Handle for async KYC processing. - */ - struct TEH_KycMeasureRunContext *kat; - - /** - * Uploaded data, in JSON. - */ - const json_t *result; - - /** - * Set by the transaction to the legitimization process row. - */ - uint64_t legi_process_row; - - /** - * Set by the transaction to the affected account payto hash. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * Set by the transaction to true if the account is for a wallet. - */ - bool is_wallet; - -}; - - -/** - * Kept in a DLL. - */ -static struct UploadContext *uc_head; - -/** - * Kept in a DLL. - */ -static struct UploadContext *uc_tail; - - -void -TEH_kyc_upload_cleanup () -{ - struct UploadContext *uc; - - while (NULL != (uc = uc_head)) - { - MHD_resume_connection (uc->rc->connection); - GNUNET_CONTAINER_DLL_remove (uc_head, - uc_tail, - uc); - } -} - - -/** - * Function called to clean up upload context. - * - * @param[in,out] rc context to clean up - */ -static void -upload_cleaner (struct TEH_RequestContext *rc) -{ - struct UploadContext *uc = rc->rh_ctx; - - if (NULL != uc->kat) - { - TEH_kyc_run_measure_cancel (uc->kat); - uc->kat = NULL; - } - if (NULL != uc->response) - { - MHD_destroy_response (uc->response); - uc->response = NULL; - } - GNUNET_free (uc); -} - - -/** - * Function called after the KYC-AML trigger is done. - * - * @param cls closure - * @param ec error code or 0 on success - * @param detail error message or NULL on success / no info - */ -static void -aml_trigger_callback ( - void *cls, - enum TALER_ErrorCode ec, - const char *detail) -{ - struct UploadContext *uc = cls; - - uc->kat = NULL; - GNUNET_assert (NULL == uc->response); - if (TALER_EC_NONE != ec) - { - uc->response_code = TALER_ErrorCode_get_http_status (ec); - if (0 == uc->response_code) - { - GNUNET_break (0); - uc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - } - GNUNET_assert (uc->response_code != UINT_MAX); - uc->response = TALER_MHD_make_error ( - ec, - detail); - } - else - { - uc->response_code = MHD_HTTP_NO_CONTENT; - uc->response = MHD_create_response_from_buffer_static ( - 0, - "" - ); - TALER_MHD_add_global_headers (uc->response, - true); - } - - MHD_resume_connection (uc->rc->connection); - GNUNET_CONTAINER_DLL_remove (uc_head, - uc_tail, - uc); - TALER_MHD_daemon_trigger (); -} - - -/** - * Do the main database transaction. - * - * @param cls closure with a `struct UploadContext` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - */ -static enum GNUNET_DB_QueryStatus -transact (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct UploadContext *uc = cls; - struct TEH_RequestContext *rc = uc->rc; - enum GNUNET_DB_QueryStatus qs; - json_t *jmeasures; - bool is_finished = false; - size_t enc_attributes_len; - void *enc_attributes; - const char *error_message; - char *form_name; - enum TALER_ErrorCode ec; - - qs = TEH_plugin->lookup_completed_legitimization ( - TEH_plugin->cls, - uc->legitimization_measure_serial_id, - uc->measure_index, - &uc->access_token, - &uc->h_payto, - &uc->is_wallet, - &jmeasures, - &is_finished, - &enc_attributes_len, - &enc_attributes); - /* FIXME: not exactly performant/elegant, should eventually - modify lookup_completed_legitimization to - return something if we are purely pending? */ - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_completed_legitimization"); - return qs; - case GNUNET_DB_STATUS_SOFT_ERROR: - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - qs = TEH_plugin->lookup_pending_legitimization ( - TEH_plugin->cls, - uc->legitimization_measure_serial_id, - &uc->access_token, - &uc->h_payto, - &jmeasures, - &is_finished, - &uc->is_wallet); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_pending_legitimization"); - return qs; - case GNUNET_DB_STATUS_SOFT_ERROR: - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - if (NULL != enc_attributes) - { - json_t *xattributes; - - xattributes - = TALER_CRYPTO_kyc_attributes_decrypt ( - &TEH_attribute_key, - enc_attributes, - enc_attributes_len); - if (json_equal (xattributes, - uc->result)) - { - /* Request is idempotent! */ - json_decref (xattributes); - GNUNET_free (enc_attributes); - json_decref (jmeasures); - if (is_finished) - { - *mhd_ret = TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - /* Note: problem below is not here, but likely some previous - upload of the attributes failed badly in an AML program. */ - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG, - "attributes known, but legitimization process failed"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - json_decref (xattributes); - GNUNET_free (enc_attributes); - json_decref (jmeasures); - /* Form was already done with with different attributes, conflict! */ - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (is_finished) - { - /* This should not be possible (is_finished but NULL==enc_attributes), - but also we should not run logic again if we are finished. */ - GNUNET_break_op (0); - json_decref (jmeasures); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - ec = TALER_KYCLOGIC_check_form (jmeasures, - uc->measure_index, - uc->result, - &form_name, - &error_message); - if (TALER_EC_NONE != ec) - { - GNUNET_break_op (0); - json_decref (jmeasures); - *mhd_ret = TALER_MHD_reply_with_ec ( - rc->connection, - ec, - error_message); - return GNUNET_DB_STATUS_HARD_ERROR; - } - json_decref (jmeasures); - - /* Setup KYC process (which we will then immediately 'finish') */ - qs = TEH_plugin->insert_kyc_requirement_process ( - TEH_plugin->cls, - &uc->h_payto, - uc->measure_index, - uc->legitimization_measure_serial_id, - form_name, - NULL, /* provider account ID */ - NULL, /* provider legi ID */ - &uc->legi_process_row); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - GNUNET_free (form_name); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_kyc_requirement_process"); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_free (form_name); - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - GNUNET_free (form_name); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "insert_kyc_requirement_process"); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - qs = TEH_kyc_store_attributes ( - uc->legi_process_row, - &uc->h_payto, - form_name, - NULL /* provider account */, - NULL /* provider legi ID */, - GNUNET_TIME_UNIT_FOREVER_ABS, /* expiration time */ - uc->result); - GNUNET_free (form_name); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "kyc_store_attributes"); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "kyc_store_attributes"); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return qs; - } - GNUNET_assert (0); - *mhd_ret = MHD_NO; - return GNUNET_DB_STATUS_HARD_ERROR; -} - - -MHD_RESULT -TEH_handler_kyc_upload ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[1]) -{ - struct UploadContext *uc = rc->rh_ctx; - const char *id = args[0]; - - if (NULL == uc) - { - const char *slash; - char dummy; - - uc = GNUNET_new (struct UploadContext); - uc->rc = rc; - uc->result = root; - rc->rh_ctx = uc; - rc->rh_cleaner = &upload_cleaner; - slash = strchr (id, '-'); - if (NULL == slash) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - rc->url); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (id, - slash - id, - &uc->access_token, - sizeof (uc->access_token))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Access token in ID is malformed"); - } - if (2 != - sscanf (slash + 1, - "%u-%llu%c", - &uc->measure_index, - &uc->legitimization_measure_serial_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "ID is malformed"); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "/kyc-upload received form submission\n"); - if ( (NULL != root) && - (! json_is_string (json_object_get (root, - "FORM_ID"))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "FORM_ID"); - } - json_dumpf (root, - stderr, - JSON_INDENT (2)); - } - if (NULL != uc->response) - { - return MHD_queue_response (rc->connection, - uc->response_code, - uc->response); - - } - - if (GNUNET_OK != - TEH_plugin->preflight (TEH_plugin->cls)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SETUP_FAILED, - NULL); - } - - { - MHD_RESULT mhd_ret = -1; - - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "kyc-upload", - TEH_MT_REQUEST_KYC_UPLOAD, - &mhd_ret, - &transact, - uc)) - return mhd_ret; - } - - uc->kat = TEH_kyc_run_measure_for_attributes ( - &rc->async_scope_id, - uc->legi_process_row, - &uc->h_payto, - uc->is_wallet, - &aml_trigger_callback, - uc); - if (NULL == uc->kat) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG, - "TEH_kyc_finished"); - } - MHD_suspend_connection (uc->rc->connection); - GNUNET_CONTAINER_DLL_insert (uc_head, - uc_tail, - uc); - return MHD_YES; -} diff --git a/src/exchange/taler-exchange-httpd_kyc-upload.h b/src/exchange/taler-exchange-httpd_kyc-upload.h @@ -1,53 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019, 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-upload.h - * @brief Handle /kyc-upload/$ID requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_KYC_UPLOAD_H -#define TALER_EXCHANGE_HTTPD_KYC_UPLOAD_H - -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Resume suspended connections, called on shutdown. - */ -void -TEH_kyc_upload_cleanup (void); - - -/** - * Handle a "/kyc-upload/$ID" request. - * - * @param rc request context - * @param root json body being uploaded - * @param args includes the ID from the URL (without "/") - * @return MHD result code - */ -MHD_RESULT -TEH_handler_kyc_upload (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[1]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.c b/src/exchange/taler-exchange-httpd_kyc-wallet.c @@ -1,336 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021, 2022, 2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-wallet.c - * @brief Handle request for wallet for KYC check. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_kyc-wallet.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Context for the request. - */ -struct KycRequestContext -{ - - /** - * Kept in a DLL. - */ - struct KycRequestContext *next; - - /** - * Kept in a DLL. - */ - struct KycRequestContext *prev; - - /** - * Handle for legitimization check. - */ - struct TEH_LegitimizationCheckHandle *lch; - - /** - * Payto URI of the reserve. - */ - struct TALER_NormalizedPayto payto_uri; - - /** - * Request context. - */ - struct TEH_RequestContext *rc; - - /** - * Response to return. Note that the response must - * be queued or destroyed by the callee. NULL - * if the legitimization check was successful and the handler should return - * a handler-specific result. - */ - struct MHD_Response *response; - - /** - * Public key of the reserve/wallet this is about. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * The wallet's public key - */ - union TALER_AccountPublicKeyP wallet_pub; - - /** - * Balance threshold crossed by the wallet. - */ - struct TALER_Amount balance; - - /** - * KYC status, with row with the legitimization requirement. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - - /** - * Smallest amount (over any timeframe) that may - * require additional KYC checks (if @a kyc.ok). - */ - struct TALER_Amount next_threshold; - - /** - * When do the current KYC rules possibly expire. - * Only valid if @a kyc.ok. - */ - struct GNUNET_TIME_Timestamp expiration_date; - - /** - * HTTP status code for @a response, or 0 - */ - unsigned int http_status; - -}; - - -/** - * Kept in a DLL. - */ -static struct KycRequestContext *krc_head; - -/** - * Kept in a DLL. - */ -static struct KycRequestContext *krc_tail; - - -void -TEH_kyc_wallet_cleanup () -{ - struct KycRequestContext *krc; - - while (NULL != (krc = krc_head)) - { - GNUNET_CONTAINER_DLL_remove (krc_head, - krc_tail, - krc); - MHD_resume_connection (krc->rc->connection); - } -} - - -/** - * Function called to iterate over KYC-relevant - * transaction amounts for a particular time range. - * Returns the wallet balance. - * - * @param cls closure, a `struct KycRequestContext` - * @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 enum GNUNET_DB_QueryStatus -balance_iterator (void *cls, - struct GNUNET_TIME_Absolute limit, - TALER_EXCHANGEDB_KycAmountCallback cb, - void *cb_cls) -{ - struct KycRequestContext *krc = cls; - enum GNUNET_GenericReturnValue ret; - - (void) limit; - ret = cb (cb_cls, - &krc->balance, - GNUNET_TIME_absolute_get ()); - GNUNET_break (GNUNET_SYSERR != ret); - if (GNUNET_OK != ret) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; -} - - -/** - * Function called with the result of a legitimization - * check. - * - * @param cls must be a `struct KycRequestContext *` - * @param lcr legitimization check result - */ -static void -legi_result_cb ( - void *cls, - const struct TEH_LegitimizationCheckResult *lcr) -{ - struct KycRequestContext *krc = cls; - - TEH_plugin->preflight (TEH_plugin->cls); - krc->lch = NULL; - krc->http_status = lcr->http_status; - krc->response = lcr->response; - krc->kyc = lcr->kyc; - krc->next_threshold = lcr->next_threshold; - krc->expiration_date = lcr->expiration_date; - GNUNET_CONTAINER_DLL_remove (krc_head, - krc_tail, - krc); - MHD_resume_connection (krc->rc->connection); - TALER_MHD_daemon_trigger (); -} - - -/** - * Function to clean up our rh_ctx in @a rc - * - * @param[in,out] rc context to clean up - */ -static void -krc_cleaner (struct TEH_RequestContext *rc) -{ - struct KycRequestContext *krc = rc->rh_ctx; - - if (NULL != krc->lch) - { - TEH_legitimization_check_cancel (krc->lch); - krc->lch = NULL; - } - GNUNET_free (krc->payto_uri.normalized_payto); - GNUNET_free (krc); -} - - -MHD_RESULT -TEH_handler_kyc_wallet ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[]) -{ - struct KycRequestContext *krc = rc->rh_ctx; - - if (NULL == krc) - { - krc = GNUNET_new (struct KycRequestContext); - krc->rc = rc; - rc->rh_ctx = krc; - rc->rh_cleaner = &krc_cleaner; - { - struct TALER_ReserveSignatureP reserve_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &reserve_sig), - GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &krc->wallet_pub.reserve_pub), - TALER_JSON_spec_amount ("balance", - TEH_currency, - &krc->balance), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; - - (void) args; - ret = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_account_setup_verify ( - &krc->wallet_pub.reserve_pub, - &krc->balance, - &reserve_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_KYC_WALLET_SIGNATURE_INVALID, - NULL); - } - } - krc->payto_uri - = TALER_reserve_make_payto (TEH_base_url, - &krc->wallet_pub.reserve_pub); - TALER_normalized_payto_hash (krc->payto_uri, - &krc->h_payto); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "h_payto of wallet %s is %s\n", - krc->payto_uri.normalized_payto, - TALER_B2S (&krc->h_payto)); - { - struct TALER_FullPayto fake_full_payto; - - GNUNET_asprintf (&fake_full_payto.full_payto, - "%s?receiver-name=wallet", - krc->payto_uri.normalized_payto); - krc->lch = TEH_legitimization_check ( - &rc->async_scope_id, - TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, - fake_full_payto, - &krc->h_payto, - &krc->wallet_pub, - &balance_iterator, - krc, - &legi_result_cb, - krc); - GNUNET_free (fake_full_payto.full_payto); - } - GNUNET_assert (NULL != krc->lch); - MHD_suspend_connection (rc->connection); - GNUNET_CONTAINER_DLL_insert (krc_head, - krc_tail, - krc); - return MHD_YES; - } - if (NULL != krc->response) - return MHD_queue_response (rc->connection, - krc->http_status, - krc->response); - if (krc->kyc.ok) - { - bool have_ts - = TALER_amount_is_valid (&krc->next_threshold); - - /* KYC not required or already satisfied */ - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_timestamp ("expiration_time", - krc->expiration_date), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("next_threshold", - have_ts - ? &krc->next_threshold - : NULL))); - } - return TEH_RESPONSE_reply_kyc_required (rc->connection, - &krc->h_payto, - &krc->kyc, - false); -} - - -/* end of taler-exchange-httpd_kyc-wallet.c */ diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.h b/src/exchange/taler-exchange-httpd_kyc-wallet.h @@ -1,52 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_kyc-wallet.h - * @brief Handle /kyc-wallet requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_KYC_WALLET_H -#define TALER_EXCHANGE_HTTPD_KYC_WALLET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Resume suspended connections, we are shutting down. - */ -void -TEH_kyc_wallet_cleanup (void); - - -/** - * Handle a "/kyc-wallet" request. Parses the "reserve_pub" EdDSA key of the - * reserve and the signature "reserve_sig" which affirms the operation. If OK, - * a KYC record is created (if missing) and the KYC status returned. - * - * @param rc request to handle - * @param root uploaded JSON data - * @param args empty array - * @return MHD result code - */ -MHD_RESULT -TEH_handler_kyc_wallet ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_management_aml-officers.c b/src/exchange/taler-exchange-httpd_management_aml-officers.c @@ -1,142 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_aml-officers.c - * @brief Handle request to update AML officer status - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * How often do we try the DB operation at most? - */ -#define MAX_RETRIES 10 - - -MHD_RESULT -TEH_handler_management_aml_officers ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct TALER_AmlOfficerPublicKeyP officer_pub; - const char *officer_name; - struct GNUNET_TIME_Timestamp change_date; - bool is_active; - bool read_only; - struct TALER_MasterSignatureP master_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("officer_pub", - &officer_pub), - GNUNET_JSON_spec_fixed_auto ("master_sig", - &master_sig), - GNUNET_JSON_spec_bool ("is_active", - &is_active), - GNUNET_JSON_spec_bool ("read_only", - &read_only), - GNUNET_JSON_spec_string ("officer_name", - &officer_name), - GNUNET_JSON_spec_timestamp ("change_date", - &change_date), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_aml_officer_status_verify ( - &officer_pub, - officer_name, - change_date, - is_active, - read_only, - &TEH_master_public_key, - &master_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID, - NULL); - } - { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp last_date; - unsigned int retries_left = MAX_RETRIES; - - do { - qs = TEH_plugin->insert_aml_officer (TEH_plugin->cls, - &officer_pub, - &master_sig, - officer_name, - is_active, - read_only, - change_date, - &last_date); - if (0 == --retries_left) - break; - } while (GNUNET_DB_STATUS_SOFT_ERROR == qs); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_aml_officer"); - } - if (GNUNET_TIME_timestamp_cmp (last_date, - >, - change_date)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT, - NULL); - } - } - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_aml-officers.c */ diff --git a/src/exchange/taler-exchange-httpd_management_auditors.c b/src/exchange/taler-exchange-httpd_management_auditors.c @@ -1,207 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020, 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_auditors.c - * @brief Handle request to add auditor. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for the #add_auditor transaction. - */ -struct AddAuditorContext -{ - /** - * Master signature to store. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Auditor public key this is about. - */ - struct TALER_AuditorPublicKeyP auditor_pub; - - /** - * Auditor URL this is about. - */ - const char *auditor_url; - - /** - * Human readable name of the auditor. - */ - const char *auditor_name; - - /** - * Timestamp for checking against replay attacks. - */ - struct GNUNET_TIME_Timestamp validity_start; - -}; - - -/** - * Function implementing database transaction to add an auditor. 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 closure with a `struct AddAuditorContext` - * @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 -add_auditor (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct AddAuditorContext *aac = cls; - struct GNUNET_TIME_Timestamp last_date; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->lookup_auditor_timestamp (TEH_plugin->cls, - &aac->auditor_pub, - &last_date); - 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, - "lookup auditor"); - return qs; - } - if ( (0 < qs) && - (GNUNET_TIME_timestamp_cmp (last_date, - >, - aac->validity_start) ) ) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (0 == qs) - qs = TEH_plugin->insert_auditor (TEH_plugin->cls, - &aac->auditor_pub, - aac->auditor_url, - aac->auditor_name, - aac->validity_start); - else - qs = TEH_plugin->update_auditor (TEH_plugin->cls, - &aac->auditor_pub, - aac->auditor_url, - aac->auditor_name, - aac->validity_start, - true); - if (qs < 0) - { - GNUNET_break (0); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "add auditor"); - return qs; - } - TEH_keys_update_states (); - return qs; -} - - -MHD_RESULT -TEH_handler_management_auditors ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct AddAuditorContext aac; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &aac.master_sig), - GNUNET_JSON_spec_fixed_auto ("auditor_pub", - &aac.auditor_pub), - TALER_JSON_spec_web_url ("auditor_url", - &aac.auditor_url), - GNUNET_JSON_spec_string ("auditor_name", - &aac.auditor_name), - GNUNET_JSON_spec_timestamp ("validity_start", - &aac.validity_start), - GNUNET_JSON_spec_end () - }; - MHD_RESULT res; - enum GNUNET_GenericReturnValue ret; - - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ - if (GNUNET_OK != - TALER_exchange_offline_auditor_add_verify ( - &aac.auditor_pub, - aac.auditor_url, - aac.validity_start, - &TEH_master_public_key, - &aac.master_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID, - NULL); - } - - ret = TEH_DB_run_transaction (connection, - "add auditor", - TEH_MT_REQUEST_OTHER, - &res, - &add_auditor, - &aac); - if (GNUNET_SYSERR == ret) - return res; - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_auditors.c */ diff --git a/src/exchange/taler-exchange-httpd_management_auditors_AP_disable.c b/src/exchange/taler-exchange-httpd_management_auditors_AP_disable.c @@ -1,196 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_auditors_AP_disable.c - * @brief Handle request to disable auditor. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for the #del_auditor transaction. - */ -struct DelAuditorContext -{ - - /** - * Auditor public key this is about. - */ - struct TALER_AuditorPublicKeyP auditor_pub; - - /** - * Auditor URL this is about. - */ - const char *auditor_url; - - /** - * Timestamp for checking against replay attacks. - */ - struct GNUNET_TIME_Timestamp validity_end; - -}; - - -/** - * Function implementing database transaction to del an auditor. 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 closure with a `struct DelAuditorContext` - * @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 -del_auditor (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct DelAuditorContext *dac = cls; - struct GNUNET_TIME_Timestamp last_date; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->lookup_auditor_timestamp (TEH_plugin->cls, - &dac->auditor_pub, - &last_date); - 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, - "lookup auditor"); - return qs; - } - if (GNUNET_TIME_timestamp_cmp (last_date, - >, - dac->validity_end)) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (0 == qs) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - qs = TEH_plugin->update_auditor (TEH_plugin->cls, - &dac->auditor_pub, - "", /* auditor URL */ - "", /* auditor name */ - dac->validity_end, - false); - if (qs < 0) - { - GNUNET_break (0); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "del auditor"); - return qs; - } - TEH_keys_update_states (); - return qs; -} - - -MHD_RESULT -TEH_handler_management_auditors_AP_disable ( - struct MHD_Connection *connection, - const struct TALER_AuditorPublicKeyP *auditor_pub, - const json_t *root) -{ - struct TALER_MasterSignatureP master_sig; - struct DelAuditorContext dac = { - .auditor_pub = *auditor_pub - }; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &master_sig), - GNUNET_JSON_spec_timestamp ("validity_end", - &dac.validity_end), - GNUNET_JSON_spec_end () - }; - MHD_RESULT res; - enum GNUNET_GenericReturnValue ret; - - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ - if (GNUNET_OK != - TALER_exchange_offline_auditor_del_verify ( - auditor_pub, - dac.validity_end, - &TEH_master_public_key, - &master_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID, - NULL); - } - - ret = TEH_DB_run_transaction (connection, - "del auditor", - TEH_MT_REQUEST_OTHER, - &res, - &del_auditor, - &dac); - if (GNUNET_SYSERR == ret) - return res; - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_auditors_AP_disable.c */ diff --git a/src/exchange/taler-exchange-httpd_management_denominations_HDP_revoke.c b/src/exchange/taler-exchange-httpd_management_denominations_HDP_revoke.c @@ -1,94 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_denominations_HDP_revoke.c - * @brief Handle denomination revocation requests. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -MHD_RESULT -TEH_handler_management_denominations_HDP_revoke ( - struct MHD_Connection *connection, - const struct TALER_DenominationHashP *h_denom_pub, - const json_t *root) -{ - struct TALER_MasterSignatureP master_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &master_sig), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_denomination_revoke_verify ( - h_denom_pub, - &TEH_master_public_key, - &master_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID, - NULL); - } - qs = TEH_plugin->insert_denomination_revocation (TEH_plugin->cls, - h_denom_pub, - &master_sig); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "denomination revocation"); - } - TEH_keys_denomination_revoke (h_denom_pub); - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_denominations_HDP_revoke.c */ diff --git a/src/exchange/taler-exchange-httpd_management_drain.c b/src/exchange/taler-exchange-httpd_management_drain.c @@ -1,195 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_drain.c - * @brief Handle request to drain profits - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Closure for the #drain transaction. - */ -struct DrainContext -{ - /** - * Fee's signature affirming the #TALER_SIGNATURE_MASTER_DRAIN_PROFITS operation. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Wire transfer identifier to use. - */ - struct TALER_WireTransferIdentifierRawP wtid; - - /** - * Account to credit. - */ - struct TALER_FullPayto payto_uri; - - /** - * Configuration section with account to debit. - */ - const char *account_section; - - /** - * Signature time. - */ - struct GNUNET_TIME_Timestamp date; - - /** - * Amount to transfer. - */ - struct TALER_Amount amount; - -}; - - -/** - * Function implementing database transaction to drain profits. 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 closure with a `struct DrainContext` - * @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 -drain (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct DrainContext *dc = cls; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->insert_drain_profit ( - TEH_plugin->cls, - &dc->wtid, - dc->account_section, - dc->payto_uri, - dc->date, - &dc->amount, - &dc->master_sig); - 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 drain profit"); - return qs; - } - return qs; -} - - -MHD_RESULT -TEH_handler_management_post_drain ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct DrainContext dc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("debit_account_section", - &dc.account_section), - TALER_JSON_spec_full_payto_uri ("credit_payto_uri", - &dc.payto_uri), - GNUNET_JSON_spec_fixed_auto ("wtid", - &dc.wtid), - GNUNET_JSON_spec_fixed_auto ("master_sig", - &dc.master_sig), - GNUNET_JSON_spec_timestamp ("date", - &dc.date), - TALER_JSON_spec_amount ("amount", - TEH_currency, - &dc.amount), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_profit_drain_verify ( - &dc.wtid, - dc.date, - &dc.amount, - dc.account_section, - dc.payto_uri, - &TEH_master_public_key, - &dc.master_sig)) - { - /* signature invalid */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID, - NULL); - } - - { - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - - res = TEH_DB_run_transaction (connection, - "insert drain profit", - TEH_MT_REQUEST_OTHER, - &ret, - &drain, - &dc); - if (GNUNET_SYSERR == res) - return ret; - } - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_drain.c */ diff --git a/src/exchange/taler-exchange-httpd_management_extensions.c b/src/exchange/taler-exchange-httpd_management_extensions.c @@ -1,300 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2021 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 <http://www.gnu.org/licenses/> - */ -/** - * @file taler-exchange-httpd_management_extensions.c - * @brief Handle request to POST /management/extensions - * @author Özgür Kesim - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_extensions.h" -#include "taler/taler_dbevents.h" - -/** - * Extension carries the necessary data for a particular extension. - * - */ -struct Extension -{ - enum TALER_Extension_Type type; - json_t *manifest; -}; - -/** - * Closure for the #set_extensions transaction - */ -struct SetExtensionsContext -{ - uint32_t num_extensions; - struct Extension *extensions; - struct TALER_MasterSignatureP extensions_sig; -}; - -/** - * Function implementing database transaction to set the manifests of - * extensions. It 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 closure with a `struct SetExtensionsContext` - * @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 -set_extensions (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct SetExtensionsContext *sec = cls; - - /* save the manifests of all extensions */ - for (uint32_t i = 0; i<sec->num_extensions; i++) - { - struct Extension *ext = &sec->extensions[i]; - const struct TALER_Extension *taler_ext; - enum GNUNET_DB_QueryStatus qs; - char *manifest; - - taler_ext = TALER_extensions_get_by_type (ext->type); - if (NULL == taler_ext) - { - /* No such extension found */ - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - manifest = json_dumps (ext->manifest, JSON_COMPACT | JSON_SORT_KEYS); - if (NULL == manifest) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_INVALID, - "convert configuration to string"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - qs = TEH_plugin->set_extension_manifest ( - TEH_plugin->cls, - taler_ext->name, - manifest); - - free (manifest); - - 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, - "save extension configuration"); - } - - /* Success, trigger event */ - { - uint32_t nbo_type = htonl (sec->extensions[i].type); - struct GNUNET_DB_EventHeaderP ev = { - .size = htons (sizeof (ev)), - .type = htons (TALER_DBEVENT_EXCHANGE_EXTENSIONS_UPDATED) - }; - - TEH_plugin->event_notify (TEH_plugin->cls, - &ev, - &nbo_type, - sizeof(nbo_type)); - } - - } - - /* All extensions configured, update the signature */ - TEH_extensions_sig = sec->extensions_sig; - TEH_extensions_signed = true; - - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* only 'success', so >=0, matters here */ -} - - -static enum GNUNET_GenericReturnValue -verify_extensions_from_json ( - const json_t *extensions, - struct SetExtensionsContext *sec) -{ - const char*name; - const struct TALER_Extension *extension; - size_t i = 0; - json_t *manifest; - - GNUNET_assert (NULL != extensions); - GNUNET_assert (json_is_object (extensions)); - - sec->num_extensions = json_object_size (extensions); - sec->extensions = GNUNET_new_array (sec->num_extensions, - struct Extension); - - json_object_foreach ((json_t *) extensions, name, manifest) - { - int critical = 0; - json_t *config; - const char *version = NULL; - - /* load and verify criticality, version, etc. */ - extension = TALER_extensions_get_by_name (name); - if (NULL == extension) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "no such extension: %s\n", name); - return GNUNET_SYSERR; - } - - if (GNUNET_OK != - TALER_extensions_parse_manifest ( - manifest, &critical, &version, &config)) - return GNUNET_SYSERR; - - if (critical != extension->critical - || 0 != strcmp (version, extension->version) // FIXME-oec: libtool compare - || NULL == config - || GNUNET_OK != extension->load_config (config, NULL)) - return GNUNET_SYSERR; - - sec->extensions[i].type = extension->type; - sec->extensions[i].manifest = json_copy (manifest); - } - - return GNUNET_OK; -} - - -MHD_RESULT -TEH_handler_management_post_extensions ( - struct MHD_Connection *connection, - const json_t *root) -{ - MHD_RESULT ret; - const json_t *extensions; - struct SetExtensionsContext sec = {0}; - struct GNUNET_JSON_Specification top_spec[] = { - GNUNET_JSON_spec_object_const ("extensions", - &extensions), - GNUNET_JSON_spec_fixed_auto ("extensions_sig", - &sec.extensions_sig), - GNUNET_JSON_spec_end () - }; - - /* Parse the top level json structure */ - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - top_spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - - /* Verify the signature */ - { - struct TALER_ExtensionManifestsHashP h_manifests; - - if (GNUNET_OK != - TALER_JSON_extensions_manifests_hash (extensions, - &h_manifests) || - GNUNET_OK != - TALER_exchange_offline_extension_manifests_hash_verify ( - &h_manifests, - &TEH_master_public_key, - &sec.extensions_sig)) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "invalid signuture"); - } - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received /management/extensions\n"); - - /* Now parse individual extensions and signatures from those objects. */ - if (GNUNET_OK != - verify_extensions_from_json (extensions, &sec)) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "invalid object"); - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received %u extensions\n", - sec.num_extensions); - - /* now run the transaction to persist the configurations */ - { - enum GNUNET_GenericReturnValue res; - - res = TEH_DB_run_transaction (connection, - "set extensions", - TEH_MT_REQUEST_OTHER, - &ret, - &set_extensions, - &sec); - - if (GNUNET_SYSERR == res) - goto CLEANUP; - } - - ret = TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - -CLEANUP: - for (unsigned int i = 0; i < sec.num_extensions; i++) - { - if (NULL != sec.extensions[i].manifest) - { - json_decref (sec.extensions[i].manifest); - } - } - GNUNET_free (sec.extensions); - return ret; -} - - -/* end of taler-exchange-httpd_management_management_post_extensions.c */ diff --git a/src/exchange/taler-exchange-httpd_management_global_fees.c b/src/exchange/taler-exchange-httpd_management_global_fees.c @@ -1,261 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020, 2021, 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_global_fees.c - * @brief Handle request to add global fee details - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Closure for the #add_fee transaction. - */ -struct AddFeeContext -{ - /** - * Fee's signature affirming the #TALER_SIGNATURE_MASTER_GLOBAL_FEES operation. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Starting period. - */ - struct GNUNET_TIME_Timestamp start_time; - - /** - * End of period. - */ - struct GNUNET_TIME_Timestamp end_time; - - /** - * Global fee amounts. - */ - struct TALER_GlobalFeeSet fees; - - /** - * When does an unmerged purse expire? - */ - struct GNUNET_TIME_Relative purse_timeout; - - /** - * When does an account without KYC expire? - */ - struct GNUNET_TIME_Relative kyc_timeout; - - /** - * When does an account history expire? - */ - struct GNUNET_TIME_Relative history_expiration; - - /** - * Number of free purses per account. - */ - uint32_t purse_account_limit; - -}; - - -/** - * Function implementing database transaction to add a fee. 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 closure with a `struct AddFeeContext` - * @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 -add_fee (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct AddFeeContext *afc = cls; - enum GNUNET_DB_QueryStatus qs; - struct TALER_GlobalFeeSet fees; - struct GNUNET_TIME_Relative purse_timeout; - struct GNUNET_TIME_Relative history_expiration; - uint32_t purse_account_limit; - - qs = TEH_plugin->lookup_global_fee_by_time ( - TEH_plugin->cls, - afc->start_time, - afc->end_time, - &fees, - &purse_timeout, - &history_expiration, - &purse_account_limit); - 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, - "lookup global fee"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) - { - if ( (GNUNET_OK == - TALER_amount_is_valid (&fees.history)) && - (0 == - TALER_global_fee_set_cmp (&fees, - &afc->fees)) ) - { - /* this will trigger the 'success' response */ - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - } - else - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH, - NULL); - } - return GNUNET_DB_STATUS_HARD_ERROR; - } - - qs = TEH_plugin->insert_global_fee ( - TEH_plugin->cls, - afc->start_time, - afc->end_time, - &afc->fees, - afc->purse_timeout, - afc->history_expiration, - afc->purse_account_limit, - &afc->master_sig); - 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 fee"); - return qs; - } - return qs; -} - - -MHD_RESULT -TEH_handler_management_post_global_fees ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct AddFeeContext afc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &afc.master_sig), - GNUNET_JSON_spec_timestamp ("fee_start", - &afc.start_time), - GNUNET_JSON_spec_timestamp ("fee_end", - &afc.end_time), - TALER_JSON_spec_amount ("history_fee", - TEH_currency, - &afc.fees.history), - TALER_JSON_spec_amount ("account_fee", - TEH_currency, - &afc.fees.account), - TALER_JSON_spec_amount ("purse_fee", - TEH_currency, - &afc.fees.purse), - GNUNET_JSON_spec_relative_time ("purse_timeout", - &afc.purse_timeout), - GNUNET_JSON_spec_relative_time ("history_expiration", - &afc.history_expiration), - GNUNET_JSON_spec_uint32 ("purse_account_limit", - &afc.purse_account_limit), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_global_fee_verify ( - afc.start_time, - afc.end_time, - &afc.fees, - afc.purse_timeout, - afc.history_expiration, - afc.purse_account_limit, - &TEH_master_public_key, - &afc.master_sig)) - { - /* signature invalid */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID, - NULL); - } - - { - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - - res = TEH_DB_run_transaction (connection, - "add global fee", - TEH_MT_REQUEST_OTHER, - &ret, - &add_fee, - &afc); - if (GNUNET_SYSERR == res) - return ret; - } - TEH_keys_update_states (); - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_global_fees.c */ diff --git a/src/exchange/taler-exchange-httpd_management_partners.c b/src/exchange/taler-exchange-httpd_management_partners.c @@ -1,132 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_partners.c - * @brief Handle request to add exchange partner - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" - - -MHD_RESULT -TEH_handler_management_partners ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct TALER_MasterPublicKeyP partner_pub; - struct GNUNET_TIME_Timestamp start_date; - struct GNUNET_TIME_Timestamp end_date; - struct GNUNET_TIME_Relative wad_frequency; - struct TALER_Amount wad_fee; - const char *partner_base_url; - struct TALER_MasterSignatureP master_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("partner_pub", - &partner_pub), - GNUNET_JSON_spec_fixed_auto ("master_sig", - &master_sig), - TALER_JSON_spec_web_url ("partner_base_url", - &partner_base_url), - TALER_JSON_spec_amount ("wad_fee", - TEH_currency, - &wad_fee), - GNUNET_JSON_spec_timestamp ("start_date", - &start_date), - GNUNET_JSON_spec_timestamp ("end_date", - &end_date), - GNUNET_JSON_spec_relative_time ("wad_frequency", - &wad_frequency), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_partner_details_verify ( - &partner_pub, - start_date, - end_date, - wad_frequency, - &wad_fee, - partner_base_url, - &TEH_master_public_key, - &master_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID, - NULL); - } - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->insert_partner (TEH_plugin->cls, - &partner_pub, - start_date, - end_date, - wad_frequency, - &wad_fee, - partner_base_url, - &master_sig); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "add_partner"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* FIXME-#7271: check for idempotency! */ - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT, - NULL); - } - } - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_partners.c */ diff --git a/src/exchange/taler-exchange-httpd_management_post_keys.c b/src/exchange/taler-exchange-httpd_management_post_keys.c @@ -1,574 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_post_keys.c - * @brief Handle request to POST /management/keys - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Denomination signature provided. - */ -struct DenomSig -{ - /** - * Hash of a denomination public key. - */ - struct TALER_DenominationHashP h_denom_pub; - - /** - * Master signature for the @e h_denom_pub. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Fee structure for this key, as per our configuration. - */ - struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; - - /** - * The full public key. - */ - struct TALER_DenominationPublicKey denom_pub; - -}; - - -/** - * Signkey signature provided. - */ -struct SigningSig -{ - /** - * Online signing key of the exchange. - */ - struct TALER_ExchangePublicKeyP exchange_pub; - - /** - * Master signature for the @e exchange_pub. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Our meta data on this key. - */ - struct TALER_EXCHANGEDB_SignkeyMetaData meta; - -}; - - -/** - * Closure for the #add_keys transaction. - */ -struct AddKeysContext -{ - - /** - * Array of @e nd_sigs denomination signatures. - */ - struct DenomSig *d_sigs; - - /** - * Array of @e ns_sigs signkey signatures. - */ - struct SigningSig *s_sigs; - - /** - * Our key state. - */ - struct TEH_KeyStateHandle *ksh; - - /** - * Length of the d_sigs array. - */ - unsigned int nd_sigs; - - /** - * Length of the n_sigs array. - */ - unsigned int ns_sigs; - -}; - - -/** - * Compare meta-data of two denomination keys for equality, - * except for the "serial" number. - * - * @param m1 meta data to compare to @a m2 - * @param m2 meta data to compare to @a m1 - * @return true if both are equal - */ -static bool -denomination_meta_cmp ( - const struct TALER_EXCHANGEDB_DenominationKeyMetaData *m1, - const struct TALER_EXCHANGEDB_DenominationKeyMetaData *m2) -{ - if ( (GNUNET_TIME_timestamp_cmp (m1->start, - !=, - m2->start)) || - (GNUNET_TIME_timestamp_cmp (m1->expire_withdraw, - !=, - m2->expire_withdraw)) || - (GNUNET_TIME_timestamp_cmp (m1->expire_deposit, - !=, - m2->expire_deposit)) || - (GNUNET_TIME_timestamp_cmp (m1->expire_legal, - !=, - m2->expire_legal)) ) - return false; - if (0 != - TALER_amount_cmp (&m1->value, - &m2->value)) - return false; - if (0 != - GNUNET_memcmp (&m1->fees, - &m2->fees)) - return false; - if (m1->age_mask.bits != - m2->age_mask.bits) - return false; - return true; -} - - -/** - * Compare meta-data of two signing keys for equality. - * - * @param m1 meta data to compare to @a m2 - * @param m2 meta data to compare to @a m1 - * @return true if both are equal - */ -static bool -signkey_meta_cmp ( - const struct TALER_EXCHANGEDB_SignkeyMetaData *m1, - const struct TALER_EXCHANGEDB_SignkeyMetaData *m2) -{ - if ( (GNUNET_TIME_timestamp_cmp (m1->start, - !=, - m2->start)) || - (GNUNET_TIME_timestamp_cmp (m1->expire_sign, - !=, - m2->expire_sign)) || - (GNUNET_TIME_timestamp_cmp (m1->expire_legal, - !=, - m2->expire_legal)) ) - return false; - return true; -} - - -/** - * Function implementing database transaction to add offline signing keys. - * 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 closure with a `struct AddKeysContext` - * @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 -add_keys (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct AddKeysContext *akc = cls; - - /* activate all denomination keys */ - for (unsigned int i = 0; i<akc->nd_sigs; i++) - { - struct DenomSig *d = &akc->d_sigs[i]; - enum GNUNET_DB_QueryStatus qs; - struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; - - /* For idempotency, check if the key is already active */ - qs = TEH_plugin->lookup_denomination_key ( - TEH_plugin->cls, - &d->h_denom_pub, - &meta); - 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, - "lookup denomination key"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - if (! denomination_meta_cmp (&d->meta, - &meta)) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_CONFLICTING_DENOMINATION_META_DATA, - "conflicting meta data previously set for the same denomination key"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Denomination key %s already active, skipping\n", - GNUNET_h2s (&d->h_denom_pub.hash)); - continue; /* skip, already known */ - } - - qs = TEH_plugin->add_denomination_key ( - TEH_plugin->cls, - &d->h_denom_pub, - &d->denom_pub, - &d->meta, - &d->master_sig); - 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, - "activate denomination key"); - return qs; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Added offline signature for denomination `%s'\n", - GNUNET_h2s (&d->h_denom_pub.hash)); - GNUNET_assert (0 != qs); - } - - for (unsigned int i = 0; i<akc->ns_sigs; i++) - { - struct SigningSig *s = &akc->s_sigs[i]; - enum GNUNET_DB_QueryStatus qs; - struct TALER_EXCHANGEDB_SignkeyMetaData meta; - - qs = TEH_plugin->lookup_signing_key ( - TEH_plugin->cls, - &s->exchange_pub, - &meta); - 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, - "lookup signing key"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - if (! signkey_meta_cmp (&s->meta, - &meta)) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_CONFLICTING_SIGNKEY_META_DATA, - "conflicting meta data previously set for the same signing key"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Signing key %s already active, skipping\n", - TALER_B2S (&s->exchange_pub)); - continue; /* skip, already known */ - } - qs = TEH_plugin->activate_signing_key ( - TEH_plugin->cls, - &s->exchange_pub, - &s->meta, - &s->master_sig); - 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, - "activate signing key"); - return qs; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Added offline signature for signing key `%s'\n", - TALER_B2S (&s->exchange_pub)); - GNUNET_assert (0 != qs); - } - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* only 'success', so >=0, matters here */ -} - - -/** - * Clean up state in @a akc, but do not free @a akc itself - * - * @param[in,out] akc state to clean up - */ -static void -cleanup_akc (struct AddKeysContext *akc) -{ - for (unsigned int i = 0; i<akc->nd_sigs; i++) - { - struct DenomSig *d = &akc->d_sigs[i]; - - TALER_denom_pub_free (&d->denom_pub); - } - GNUNET_free (akc->d_sigs); - GNUNET_free (akc->s_sigs); -} - - -MHD_RESULT -TEH_handler_management_post_keys ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct AddKeysContext akc = { 0 }; - const json_t *denom_sigs; - const json_t *signkey_sigs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("denom_sigs", - &denom_sigs), - GNUNET_JSON_spec_array_const ("signkey_sigs", - &signkey_sigs), - GNUNET_JSON_spec_end () - }; - MHD_RESULT ret; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received POST /management/keys request\n"); - - akc.ksh = TEH_keys_get_state_for_management_only (); /* may start its own transaction, thus must be done here, before we run ours! */ - if (NULL == akc.ksh) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - "no key state (not even for management)"); - } - - akc.nd_sigs = json_array_size (denom_sigs); - akc.d_sigs = GNUNET_new_array (akc.nd_sigs, - struct DenomSig); - for (unsigned int i = 0; i<akc.nd_sigs; i++) - { - struct DenomSig *d = &akc.d_sigs[i]; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &d->master_sig), - GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &d->h_denom_pub), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - json_array_get (denom_sigs, - i), - ispec); - if (GNUNET_OK != res) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failure to handle /management/keys\n"); - cleanup_akc (&akc); - return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - } - - res = TEH_keys_load_fees (akc.ksh, - &d->h_denom_pub, - &d->denom_pub, - &d->meta); - switch (res) - { - case GNUNET_SYSERR: - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, - GNUNET_h2s (&d->h_denom_pub.hash)); - cleanup_akc (&akc); - return ret; - case GNUNET_NO: - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, - GNUNET_h2s (&d->h_denom_pub.hash)); - cleanup_akc (&akc); - return ret; - case GNUNET_OK: - break; - } - /* check signature is valid */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_denom_validity_verify ( - &d->h_denom_pub, - d->meta.start, - d->meta.expire_withdraw, - d->meta.expire_deposit, - d->meta.expire_legal, - &d->meta.value, - &d->meta.fees, - &TEH_master_public_key, - &d->master_sig)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID, - GNUNET_h2s (&d->h_denom_pub.hash)); - cleanup_akc (&akc); - return ret; - } - } - - akc.ns_sigs = json_array_size (signkey_sigs); - akc.s_sigs = GNUNET_new_array (akc.ns_sigs, - struct SigningSig); - for (unsigned int i = 0; i<akc.ns_sigs; i++) - { - struct SigningSig *s = &akc.s_sigs[i]; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &s->master_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &s->exchange_pub), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - json_array_get (signkey_sigs, - i), - ispec); - if (GNUNET_OK != res) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failure to handle /management/keys\n"); - cleanup_akc (&akc); - return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - } - res = TEH_keys_get_timing (&s->exchange_pub, - &s->meta); - switch (res) - { - case GNUNET_SYSERR: - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, - TALER_B2S (&s->exchange_pub)); - cleanup_akc (&akc); - return ret; - case GNUNET_NO: - /* For idempotency, check if the key is already active */ - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN, - TALER_B2S (&s->exchange_pub)); - cleanup_akc (&akc); - return ret; - case GNUNET_OK: - break; - } - - /* check signature is valid */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_signkey_validity_verify ( - &s->exchange_pub, - s->meta.start, - s->meta.expire_sign, - s->meta.expire_legal, - &TEH_master_public_key, - &s->master_sig)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID, - TALER_B2S (&s->exchange_pub)); - cleanup_akc (&akc); - return ret; - } - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received %u denomination and %u signing key signatures\n", - akc.nd_sigs, - akc.ns_sigs); - { - enum GNUNET_GenericReturnValue res; - - res = TEH_DB_run_transaction (connection, - "add keys", - TEH_MT_REQUEST_OTHER, - &ret, - &add_keys, - &akc); - cleanup_akc (&akc); - if (GNUNET_SYSERR == res) - return ret; - } - TEH_keys_update_states (); - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_management_post_keys.c */ diff --git a/src/exchange/taler-exchange-httpd_management_signkey_EP_revoke.c b/src/exchange/taler-exchange-httpd_management_signkey_EP_revoke.c @@ -1,93 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_signkey_EP_revoke.c - * @brief Handle exchange online signing key revocation requests. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -MHD_RESULT -TEH_handler_management_signkeys_EP_revoke ( - struct MHD_Connection *connection, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const json_t *root) -{ - struct TALER_MasterSignatureP master_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &master_sig), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_signkey_revoke_verify (exchange_pub, - &TEH_master_public_key, - &master_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID, - NULL); - } - qs = TEH_plugin->insert_signkey_revocation (TEH_plugin->cls, - exchange_pub, - &master_sig); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "signkey revocation"); - } - TEH_keys_update_states (); - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_signkey_HDP_revoke.c */ diff --git a/src/exchange/taler-exchange-httpd_management_wire_disable.c b/src/exchange/taler-exchange-httpd_management_wire_disable.c @@ -1,207 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_wire_disable.c - * @brief Handle request to disable wire account. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for the #del_wire transaction. - */ -struct DelWireContext -{ - /** - * Master signature affirming the WIRE DEL operation - * (includes timestamp). - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Payto:// URI this is about. - */ - struct TALER_FullPayto payto_uri; - - /** - * Timestamp for checking against replay attacks. - */ - struct GNUNET_TIME_Timestamp validity_end; - -}; - - -/** - * Function implementing database transaction to del an wire. 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 closure with a `struct DelWireContext` - * @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 -del_wire (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct DelWireContext *awc = cls; - struct GNUNET_TIME_Timestamp last_date; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->lookup_wire_timestamp (TEH_plugin->cls, - awc->payto_uri, - &last_date); - 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, - "lookup wire"); - return qs; - } - if (GNUNET_TIME_timestamp_cmp (last_date, - >, - awc->validity_end)) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - qs = TEH_plugin->update_wire (TEH_plugin->cls, - awc->payto_uri, - NULL, - NULL, - NULL, - NULL, - NULL, - awc->validity_end, - NULL, - NULL, - 0, - false); - if (qs < 0) - { - GNUNET_break (0); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "del wire"); - return qs; - } - return qs; -} - - -MHD_RESULT -TEH_handler_management_post_wire_disable ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct DelWireContext awc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig_del", - &awc.master_sig), - TALER_JSON_spec_full_payto_uri ("payto_uri", - &awc.payto_uri), - GNUNET_JSON_spec_timestamp ("validity_end", - &awc.validity_end), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - if (GNUNET_OK != - TALER_exchange_offline_wire_del_verify ( - awc.payto_uri, - awc.validity_end, - &TEH_master_public_key, - &awc.master_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID, - NULL); - } - - { - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - - res = TEH_DB_run_transaction (connection, - "del wire", - TEH_MT_REQUEST_OTHER, - &ret, - &del_wire, - &awc); - if (GNUNET_SYSERR == res) - return ret; - } - TEH_wire_update_state (); - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_wire_disable.c */ diff --git a/src/exchange/taler-exchange-httpd_management_wire_enable.c b/src/exchange/taler-exchange-httpd_management_wire_enable.c @@ -1,346 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_wire_enable.c - * @brief Handle request to add wire account. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for the #add_wire transaction. - */ -struct AddWireContext -{ - /** - * Master signature affirming the WIRE ADD operation - * (includes timestamp). - */ - struct TALER_MasterSignatureP master_sig_add; - - /** - * Master signature to share with clients affirming the - * wire details of the bank. - */ - struct TALER_MasterSignatureP master_sig_wire; - - /** - * Payto:// URI this is about. - */ - struct TALER_FullPayto payto_uri; - - /** - * (optional) address of a conversion service for this account. - */ - const char *conversion_url; - - /** - * (optional) address of an open banking gateway service for this account. - */ - const char *open_banking_gateway; - - /** - * (optional) address of a wire transfer gateway service for this account. - */ - const char *wire_transfer_gateway; - - /** - * Restrictions imposed when crediting this account. - */ - const json_t *credit_restrictions; - - /** - * Restrictions imposed when debiting this account. - */ - const json_t *debit_restrictions; - - /** - * Timestamp for checking against replay attacks. - */ - struct GNUNET_TIME_Timestamp validity_start; - - /** - * Label to use for this bank. Default is empty. - */ - const char *bank_label; - - /** - * Priority of the bank in the list. Default 0. - */ - int64_t priority; - -}; - - -/** - * Function implementing database transaction to add an wire. 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 closure with a `struct AddWireContext` - * @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 -add_wire (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct AddWireContext *awc = cls; - struct GNUNET_TIME_Timestamp last_date; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->lookup_wire_timestamp (TEH_plugin->cls, - awc->payto_uri, - &last_date); - 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, - "lookup wire"); - return qs; - } - if ( (0 < qs) && - (GNUNET_TIME_timestamp_cmp (last_date, - >, - awc->validity_start)) ) - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - qs = TEH_plugin->insert_wire (TEH_plugin->cls, - awc->payto_uri, - awc->conversion_url, - awc->open_banking_gateway, - awc->wire_transfer_gateway, - awc->debit_restrictions, - awc->credit_restrictions, - awc->validity_start, - &awc->master_sig_wire, - awc->bank_label, - awc->priority); - else - qs = TEH_plugin->update_wire (TEH_plugin->cls, - awc->payto_uri, - awc->conversion_url, - awc->open_banking_gateway, - awc->wire_transfer_gateway, - awc->debit_restrictions, - awc->credit_restrictions, - awc->validity_start, - &awc->master_sig_wire, - awc->bank_label, - awc->priority, - true); - if (qs < 0) - { - GNUNET_break (0); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "add wire"); - return qs; - } - return qs; -} - - -MHD_RESULT -TEH_handler_management_post_wire ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct AddWireContext awc = { - .conversion_url = NULL - }; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_full_payto_uri ("payto_uri", - &awc.payto_uri), - GNUNET_JSON_spec_fixed_auto ("master_sig_wire", - &awc.master_sig_wire), - GNUNET_JSON_spec_fixed_auto ("master_sig_add", - &awc.master_sig_add), - GNUNET_JSON_spec_timestamp ("validity_start", - &awc.validity_start), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("conversion_url", - &awc.conversion_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("open_banking_gateway", - &awc.open_banking_gateway), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("wire_transfer_gateway", - &awc.wire_transfer_gateway), - NULL), - GNUNET_JSON_spec_array_const ("credit_restrictions", - &awc.credit_restrictions), - GNUNET_JSON_spec_array_const ("debit_restrictions", - &awc.debit_restrictions), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("bank_label", - &awc.bank_label), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_int64 ("priority", - &awc.priority), - NULL), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - { - char *msg = TALER_payto_validate (awc.payto_uri); - - if (NULL != msg) - { - MHD_RESULT ret; - - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PAYTO_URI_MALFORMED, - msg); - GNUNET_JSON_parse_free (spec); - GNUNET_free (msg); - return ret; - } - } - if (GNUNET_OK != - TALER_exchange_offline_wire_add_verify ( - awc.payto_uri, - awc.conversion_url, - awc.open_banking_gateway, - awc.wire_transfer_gateway, - awc.debit_restrictions, - awc.credit_restrictions, - awc.validity_start, - &TEH_master_public_key, - &awc.master_sig_add)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID, - NULL); - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_wire_signature_check ( - awc.payto_uri, - awc.conversion_url, - awc.open_banking_gateway, - awc.wire_transfer_gateway, - awc.debit_restrictions, - awc.credit_restrictions, - &TEH_master_public_key, - &awc.master_sig_wire)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID, - NULL); - } - { - char *wire_method; - - wire_method = TALER_payto_get_method (awc.payto_uri.full_payto); - if (NULL == wire_method) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "payto:// URI `%s' is malformed\n", - awc.payto_uri.full_payto); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payto_uri"); - } - GNUNET_free (wire_method); - } - - { - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - - res = TEH_DB_run_transaction (connection, - "add wire", - TEH_MT_REQUEST_OTHER, - &ret, - &add_wire, - &awc); - GNUNET_JSON_parse_free (spec); - if (GNUNET_SYSERR == res) - return ret; - } - TEH_wire_update_state (); - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_wire.c */ diff --git a/src/exchange/taler-exchange-httpd_management_wire_fees.c b/src/exchange/taler-exchange-httpd_management_wire_fees.c @@ -1,230 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020, 2021, 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_management_wire_fees.c - * @brief Handle request to add wire fee details - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for the #add_fee transaction. - */ -struct AddFeeContext -{ - /** - * Fee's signature affirming the #TALER_SIGNATURE_MASTER_WIRE_FEES operation. - */ - struct TALER_MasterSignatureP master_sig; - - /** - * Wire method this is about. - */ - const char *wire_method; - - /** - * Starting period. - */ - struct GNUNET_TIME_Timestamp start_time; - - /** - * End of period. - */ - struct GNUNET_TIME_Timestamp end_time; - - /** - * Wire fee amounts. - */ - struct TALER_WireFeeSet fees; - -}; - - -/** - * Function implementing database transaction to add a fee. 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 closure with a `struct AddFeeContext` - * @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 -add_fee (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct AddFeeContext *afc = cls; - enum GNUNET_DB_QueryStatus qs; - struct TALER_WireFeeSet fees; - - qs = TEH_plugin->lookup_wire_fee_by_time ( - TEH_plugin->cls, - afc->wire_method, - afc->start_time, - afc->end_time, - &fees); - 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, - "lookup wire fee"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) - { - if ( (GNUNET_OK == - TALER_amount_is_valid (&fees.wire)) && - (0 == - TALER_wire_fee_set_cmp (&fees, - &afc->fees)) ) - { - /* this will trigger the 'success' response */ - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - } - else - { - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH, - NULL); - } - return GNUNET_DB_STATUS_HARD_ERROR; - } - - qs = TEH_plugin->insert_wire_fee ( - TEH_plugin->cls, - afc->wire_method, - afc->start_time, - afc->end_time, - &afc->fees, - &afc->master_sig); - 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 fee"); - return qs; - } - return qs; -} - - -MHD_RESULT -TEH_handler_management_post_wire_fees ( - struct MHD_Connection *connection, - const json_t *root) -{ - struct AddFeeContext afc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("master_sig", - &afc.master_sig), - GNUNET_JSON_spec_string ("wire_method", - &afc.wire_method), - GNUNET_JSON_spec_timestamp ("fee_start", - &afc.start_time), - GNUNET_JSON_spec_timestamp ("fee_end", - &afc.end_time), - TALER_JSON_spec_amount ("wire_fee", - TEH_currency, - &afc.fees.wire), - TALER_JSON_spec_amount ("closing_fee", - TEH_currency, - &afc.fees.closing), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_exchange_offline_wire_fee_verify ( - afc.wire_method, - afc.start_time, - afc.end_time, - &afc.fees, - &TEH_master_public_key, - &afc.master_sig)) - { - /* signature invalid */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID, - NULL); - } - - { - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - - res = TEH_DB_run_transaction (connection, - "add wire fee", - TEH_MT_REQUEST_OTHER, - &ret, - &add_fee, - &afc); - if (GNUNET_SYSERR == res) - return ret; - } - TEH_wire_update_state (); - return TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_management_wire_fees.c */ diff --git a/src/exchange/taler-exchange-httpd_melt.c b/src/exchange/taler-exchange-httpd_melt.c @@ -1,1864 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_melt.c - * @brief Handle /melt requests - * @note This endpoint is active since vDOLDPLUS of the protocol API - * @author Özgür Kesim - */ - -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler-exchange-httpd.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_melt.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" -#include "taler/taler_util.h" - -/** - * The different type of errors that might occur, sorted by name. - * Some of them require idempotency checks, which are marked - * in @e idempotency_check_required below. - */ -enum MeltError -{ - MELT_ERROR_NONE = 0, - MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID, - MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - MELT_ERROR_AMOUNT_OVERFLOW, - MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, - MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT, - MELT_ERROR_BLINDING_SEED_REQUIRED, - MELT_ERROR_COIN_CIPHER_MISMATCH, - MELT_COIN_CONFLICTING_DENOMINATION_KEY, - MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE, - MELT_ERROR_COIN_SIGNATURE_INVALID, - MELT_ERROR_COIN_UNKNOWN, - MELT_ERROR_CONFIRMATION_SIGN, - MELT_ERROR_CRYPTO_HELPER, - MELT_ERROR_DB_FETCH_FAILED, - MELT_ERROR_DB_INVARIANT_FAILURE, - MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE, - MELT_ERROR_DB_PREFLIGHT_FAILURE, - MELT_ERROR_DENOMINATION_EXPIRED, - MELT_ERROR_DENOMINATION_KEY_UNKNOWN, - MELT_ERROR_DENOMINATION_REVOKED, - MELT_ERROR_DENOMINATION_SIGN, - MELT_ERROR_DENOMINATION_SIGNATURE_INVALID, - MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - MELT_ERROR_DUPLICATE_PLANCHET, - MELT_ERROR_DUPLICATE_TRANSFER_PUB, - MELT_ERROR_INSUFFICIENT_FUNDS, - MELT_ERROR_KEYS_MISSING, - MELT_ERROR_FEES_EXCEED_CONTRIBUTION, - MELT_ERROR_NONCE_RESUSE, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, -}; - -/** - * With the bits set in this value will be mark the errors - * that require a check for idempotency before actually - * returning an error. - */ -static const uint64_t idempotency_check_required = - 0 - | (1 << MELT_ERROR_DENOMINATION_EXPIRED) - | (1 << MELT_ERROR_DENOMINATION_KEY_UNKNOWN) - | (1 << MELT_ERROR_DENOMINATION_REVOKED) - | (1 << MELT_ERROR_INSUFFICIENT_FUNDS) /* TODO: is this still correct? Compare exchange_do_refresh.sql */ - | (1 << MELT_ERROR_KEYS_MISSING); - -#define IDEMPOTENCY_CHECK_REQUIRED(error) \ - (0 != (idempotency_check_required & (1 << (error)))) - -/** - * Context for a /melt request - */ -struct MeltContext -{ - - /** - * This struct is kept in a DLL. - */ - struct MeltContext *prev; - struct MeltContext *next; - - /** - * Processing phase we are in. - * The ordering here partially matters, as we progress through - * them by incrementing the phase in the happy path. - */ - enum MeltPhase - { - MELT_PHASE_PARSE, - MELT_PHASE_CHECK_MELT_VALID, - MELT_PHASE_CHECK_KEYS, - MELT_PHASE_CHECK_COIN_SIGNATURE, - MELT_PHASE_PREPARE_TRANSACTION, - MELT_PHASE_RUN_TRANSACTION, - MELT_PHASE_GENERATE_REPLY_SUCCESS, - MELT_PHASE_GENERATE_REPLY_ERROR, - MELT_PHASE_RETURN_NO, - MELT_PHASE_RETURN_YES, - } phase; - - - /** - * Request context - */ - const struct TEH_RequestContext *rc; - - /** - * Current time for the DB transaction. - */ - struct GNUNET_TIME_Timestamp now; - - /** - * The current key state - */ - struct TEH_KeyStateHandle *ksh; - - /** - * The melted coin's denomination key - */ - struct TEH_DenominationKey *melted_coin_denom; - - /** - * Set to true if this coin's denomination was revoked and the operation - * is thus only allowed for zombie coins where the transaction - * history includes a #TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP. - * - * TODO: find a better terminology. The sentences in the comments containing - * "zombie" make semantically _no sense_! - */ - bool zombie_required; - - /** - * We already checked and noticed that the coin is known. Hence we - * can skip the "ensure_coin_known" step of the transaction. - */ - bool coin_is_known; - - /** - * UUID of the coin in the known_coins table. - */ - uint64_t known_coin_id; - - /** - * Captures all parameters provided in the JSON request - */ - struct - { - - /** - * All fields (from the request or computed) - * that we persist in the database. - */ - struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh; - - /** - * In some error cases we check for idempotency. - * If we find an entry in the database, we mark this here. - */ - bool is_idempotent; - - /** - * In some error conditions the request is checked - * for idempotency and the result from the database - * is stored here. - */ - struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh_idem; - - /** - * True if @e blinding_seed is missing in the request - */ - bool no_blinding_seed; - - /** - * Array @e persis.num_coins of hashes of the public keys - * of the denominations to refresh. - */ - struct TALER_DenominationHashP *denoms_h; - - /** - * Array of @e refresh.num_coins blinded coin planchets, arranged - * in runs of @e refresh.num_coins coins, [0..num_coins)..[0..num_coins), - * one for each kappa value. - */ - struct TALER_BlindedPlanchet *planchets[TALER_CNC_KAPPA]; - - /** - * @since vDOLDPLUS - * #TALER_CNC_KAPPA arrays of @e refresh.num_coins transfer public keys - * in runs of @e num_coins coins, [0..num_coins)..[0..num_coins), - * one for each kappa value. - * - * MAYBE null. If the client has NOT provided the transfer_pubs in the request, - * @e refresh.is_v27_refresh will be true. - * - * TODO: Once v27 clients are gone, this MUST NOT be nulls. - */ - struct TALER_TransferPublicKeyP *transfer_pubs[TALER_CNC_KAPPA]; - - /** - * #TALER_CNC_KAPPA hashes of the batches of @e num_coins coins. - */ - struct TALER_KappaHashBlindedPlanchetsP kappa_planchets_h; - - /** - * Array @e withdraw.num_r_pubs of indices into @e denoms_h - * of CS denominations. - */ - uint32_t *cs_indices; - - /** - * Total (over all coins) amount (excluding fee) committed for the refresh - */ - struct TALER_Amount amount; - - } request; - - /** - * Errors occurring during evaluation of the request are captured in this - * struct. In phase WITHDRAW_PHASE_GENERATE_REPLY_ERROR an appropriate error - * message is prepared and sent to the client. - */ - struct - { - /* The (internal) error code */ - enum MeltError code; - - /** - * Some errors require details to be sent to the client. - * These are captured in this union. - * Each field is named according to the error that is using it, except - * commented otherwise. - */ - union - { - const char *request_parameter_malformed; - - /** - * For all errors related to a particular denomination, i.e. - * #MELT_ERROR_DENOMINATION_KEY_UNKNOWN, - * #MELT_ERROR_DENOMINATION_EXPIRED, - * #MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - * #MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - * we use this one field. - */ - struct TALER_DenominationHashP denom_h; - - const char *db_fetch_context; - - enum TALER_ErrorCode ec_confirmation_sign; - - enum TALER_ErrorCode ec_denomination_sign; - - /* remaining value of the coin */ - struct TALER_Amount insufficient_funds; - - } details; - } error; -}; - -/** - * The following macros set the given error code, - * set the phase to Melt_PHASE_GENERATE_REPLY_ERROR, - * and optionally set the given field (with an optionally given value). - */ -#define SET_ERROR(mc, ec) \ - do \ - { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ - (mc)->error.code = (ec); \ - (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) - -#define SET_ERROR_WITH_FIELD(mc, ec, field) \ - do \ - { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ - (mc)->error.code = (ec); \ - (mc)->error.details.field = (field); \ - (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) - -#define SET_ERROR_WITH_DETAIL(mc, ec, field, value) \ - do \ - { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ - (mc)->error.code = (ec); \ - (mc)->error.details.field = (value); \ - (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) - - -/** - * All melt context is kept in a DLL. - */ -static struct MeltContext *mc_head; -static struct MeltContext *mc_tail; - -void -TEH_melt_cleanup () -{ - struct MeltContext *mc; - - while (NULL != (mc = mc_head)) - { - GNUNET_CONTAINER_DLL_remove (mc_head, - mc_tail, - mc); - MHD_resume_connection (mc->rc->connection); - } -} - - -/** - * Terminate the main loop by returning the final result. - * - * @param[in,out] mc context to update phase for - * @param mres MHD status to return - */ -static void -finish_loop (struct MeltContext *mc, - MHD_RESULT mres) -{ - mc->phase = (MHD_YES == mres) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; -} - - -/** - * Free information in @a re, but not @a re itself. - * - * @param[in] re refresh data to free - */ -static void -free_refresh (struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *re) -{ - if (NULL != re->denom_sigs) - { - for (size_t i = 0; i<re->num_coins; i++) - TALER_blinded_denom_sig_free (&re->denom_sigs[i]); - GNUNET_free (re->denom_sigs); - } - GNUNET_free (re->cs_r_values); - GNUNET_free (re->denom_serials); - GNUNET_free (re->denom_pub_hashes); - TALER_denom_sig_free (&re->coin.denom_sig); -} - - -/** - * Cleanup routine for melt request. - * The function is called upon completion of the request - * that should clean up @a rh_ctx. - * - * @param rc request context to clean up - */ -static void -clean_melt_rc (struct TEH_RequestContext *rc) -{ - struct MeltContext *mc = rc->rh_ctx; - - GNUNET_free (mc->request.denoms_h); - for (uint8_t k = 0; k<TALER_CNC_KAPPA; k++) - { - for (size_t i = 0; i<mc->request.refresh.num_coins; i++) - TALER_blinded_planchet_free (&mc->request.planchets[k][i]); - GNUNET_free (mc->request.planchets[k]); - if (! mc->request.refresh.is_v27_refresh) - GNUNET_free (mc->request.transfer_pubs[k]); - } - free_refresh (&mc->request.refresh); - if (mc->request.is_idempotent) - free_refresh (&mc->request.refresh_idem); - GNUNET_free (mc->request.cs_indices); - GNUNET_free (mc); -} - - -/** - * Creates a new context for the incoming melt request - * - * @param mc melt request context - * @param root json body of the request - */ -static void -phase_parse_request ( - struct MeltContext *mc, - const json_t *root) -{ - const json_t *j_denoms_h; - const json_t *j_coin_evs; - const json_t *j_transfer_pubs; - enum GNUNET_GenericReturnValue res; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("old_coin_pub", - &mc->request.refresh.coin.coin_pub), - GNUNET_JSON_spec_fixed_auto ("old_denom_pub_h", - &mc->request.refresh.coin.denom_pub_hash), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("old_age_commitment_h", - &mc->request.refresh.coin.h_age_commitment), - &mc->request.refresh.coin.no_age_commitment), - TALER_JSON_spec_denom_sig ("old_denom_sig", - &mc->request.refresh.coin.denom_sig), - GNUNET_JSON_spec_fixed_auto ("refresh_seed", - &mc->request.refresh.refresh_seed), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("blinding_seed", - &mc->request.refresh.blinding_seed), - &mc->request.refresh.no_blinding_seed), - TALER_JSON_spec_amount ("value_with_fee", - TEH_currency, - &mc->request.refresh.amount_with_fee), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("transfer_pubs", - &j_transfer_pubs), - &mc->request.refresh.is_v27_refresh), - GNUNET_JSON_spec_array_const ("denoms_h", - &j_denoms_h), - GNUNET_JSON_spec_array_const ("coin_evs", - &j_coin_evs), - GNUNET_JSON_spec_fixed_auto ("confirm_sig", - &mc->request.refresh.coin_sig), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (mc->rc->connection, - root, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - mc->phase = (GNUNET_NO == res) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; - return; - } - - /* validate array size */ - GNUNET_static_assert ( - TALER_MAX_COINS < INT_MAX / TALER_CNC_KAPPA); - - mc->request.refresh.num_coins = json_array_size (j_denoms_h); - if (0 == mc->request.refresh.num_coins) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "denoms_h must not be empty"); - return; - } - if (TALER_MAX_COINS < mc->request.refresh.num_coins) - { - /** - * The wallet had committed to more than the maximum coins allowed, the - * reserve has been charged, but now the user can not melt any money - * from it. Note that the user can't get their money back in this case! - */ - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "maximum number of coins that can be refreshed has been exceeded"); - return; - } - if (TALER_CNC_KAPPA != json_array_size (j_coin_evs)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "coin_evs must be an array of length "TALER_CNC_KAPPA_STR); - return; - } - if (! mc->request.refresh.is_v27_refresh && - (TALER_CNC_KAPPA != json_array_size (j_transfer_pubs))) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "transfer_pubs must be an array of length "TALER_CNC_KAPPA_STR); - return; - } - - - /* Extract the denomination hashes */ - { - size_t idx; - json_t *value; - - mc->request.denoms_h - = GNUNET_new_array (mc->request.refresh.num_coins, - struct TALER_DenominationHashP); - - json_array_foreach (j_denoms_h, idx, value) { - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &mc->request.denoms_h[idx]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (mc->rc->connection, - value, - ispec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - mc->phase = (GNUNET_NO == res) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; - return; - } - } - } - - /* Parse blinded envelopes. */ - { - json_t *j_kappa_planchets; - size_t kappa; - struct GNUNET_HashContext *ctx; - - /* ctx to calculate the planchet_h */ - ctx = GNUNET_CRYPTO_hash_context_start (); - GNUNET_assert (NULL != ctx); - - json_array_foreach (j_coin_evs, kappa, j_kappa_planchets) - { - json_t *j_cev; - size_t idx; - - if (mc->request.refresh.num_coins != json_array_size (j_kappa_planchets)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "coin_evs[i][] size"); - return; - } - - mc->request.planchets[kappa] = - GNUNET_new_array (mc->request.refresh.num_coins, - struct TALER_BlindedPlanchet); - - json_array_foreach (j_kappa_planchets, idx, j_cev) - { - /* Now parse the individual envelopes and calculate the hash of - * the commitment along the way. */ - struct GNUNET_JSON_Specification kspec[] = { - TALER_JSON_spec_blinded_planchet (NULL, - &mc->request.planchets[kappa][idx]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (mc->rc->connection, - j_cev, - kspec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - mc->phase = (GNUNET_NO == res) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; - return; - } - } - /* Save the hash of the batch of planchets for index kappa */ - TALER_wallet_blinded_planchets_hash ( - mc->request.refresh.num_coins, - mc->request.planchets[kappa], - mc->request.denoms_h, - &mc->request.kappa_planchets_h.tuple[kappa]); - GNUNET_CRYPTO_hash_context_read ( - ctx, - &mc->request.kappa_planchets_h.tuple[kappa], - sizeof(mc->request.kappa_planchets_h.tuple[kappa])); - } - /* Finally calculate the total hash over all planchets */ - GNUNET_CRYPTO_hash_context_finish ( - ctx, - &mc->request.refresh.planchets_h.hash); - } - /* Check for duplicate planchets. Technically a bug on - * the client side that is harmless for us, but still - * not allowed per protocol - */ - { - size_t max_idx = TALER_CNC_KAPPA * mc->request.refresh.num_coins; - - for (size_t I = 0; I < max_idx - 1; I++) - { - size_t ki = I / mc->request.refresh.num_coins; - size_t ni = I % mc->request.refresh.num_coins; - - for (size_t J = I + 1; J < max_idx; J++) - { - size_t kj = J / mc->request.refresh.num_coins; - size_t nj = J % mc->request.refresh.num_coins; - - if (0 == TALER_blinded_planchet_cmp ( - &mc->request.planchets[ki][ni], - &mc->request.planchets[kj][nj])) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_DUPLICATE_TRANSFER_PUB); - return; - } - } - } - } - - /* Parse the transfer public keys, if applicable */ - if (! mc->request.refresh.is_v27_refresh) - { - json_t *j_ktp; - size_t kappa; - size_t max_idx; - - json_array_foreach (j_transfer_pubs, kappa, j_ktp) - { - if (mc->request.refresh.num_coins != - json_array_size (j_ktp)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "transfer_pubs[i][] size"); - return; - } - - mc->request.transfer_pubs[kappa] = - GNUNET_new_array (mc->request.refresh.num_coins, - struct TALER_TransferPublicKeyP); - - /* Parse the batch of @e num_coins transfer public keys - * at index kappa */ - { - struct GNUNET_JSON_Specification ktp_spec[] = { - TALER_JSON_spec_array_fixed (NULL, - mc->request.refresh.num_coins, - mc->request.transfer_pubs[kappa], - sizeof(*mc->request.transfer_pubs[kappa]) - ), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (mc->rc->connection, - j_ktp, - ktp_spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - mc->phase = (GNUNET_NO == res) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; - return; - } - } - } - /* Check for duplicate transfer public keys. Technically a bug on - * the client side that is harmless for us, but still - * not allowed per protocol - */ - max_idx = TALER_CNC_KAPPA * mc->request.refresh.num_coins; - - for (size_t I = 0; I < max_idx - 1; I++) - { - size_t ki = I / mc->request.refresh.num_coins; - size_t ni = I % mc->request.refresh.num_coins; - - for (size_t J = I + 1; J < max_idx; J++) - { - size_t kj = J / mc->request.refresh.num_coins; - size_t nj = J % mc->request.refresh.num_coins; - - if (0 == GNUNET_memcmp ( - &mc->request.transfer_pubs[ki][ni], - &mc->request.transfer_pubs[kj][nj])) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_DUPLICATE_TRANSFER_PUB); - return; - } - } - } - } - - mc->ksh = TEH_keys_get_state (); - if (NULL == mc->ksh) - { - GNUNET_break (0); - SET_ERROR (mc, - MELT_ERROR_KEYS_MISSING); - return; - } - mc->phase = MELT_PHASE_CHECK_MELT_VALID; -} - - -/** - * Check if the given denomination is still or already valid, has not been - * revoked and potentically supports age restriction. - * - * @param[in,out] mc context for the melt operation - * @param denom_h Hash of the denomination key to check - * @param[out] pdk denomination key found, might be NULL - * @return #GNUNET_OK when denomation was found and valid, - * #GNUNET_NO when denomination is not valid at this time - * #GNUNET_SYSERR otherwise (denomination invalid), with finish_loop called. - */ -static enum GNUNET_GenericReturnValue -find_denomination ( - struct MeltContext *mc, - const struct TALER_DenominationHashP *denom_h, - struct TEH_DenominationKey **pdk) -{ - struct TEH_DenominationKey *dk; - - *pdk = NULL; - GNUNET_assert (NULL != mc->ksh); - dk = TEH_keys_denomination_by_hash_from_state (mc->ksh, - denom_h, - NULL, - NULL); - if (NULL == dk) - { - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DENOMINATION_KEY_UNKNOWN, - denom_h, - *denom_h); - return GNUNET_SYSERR; - } - *pdk = dk; - - if (GNUNET_TIME_absolute_is_past ( - dk->meta.expire_withdraw.abs_time)) - { - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DENOMINATION_EXPIRED, - denom_h, - *denom_h); - /** - * Note that we return GNUNET_NO here. - * This way phase_check_melt_valid can react - * to it as a non-error case and do the zombie check. - */ - return GNUNET_NO; - } - - if (GNUNET_TIME_absolute_is_future ( - dk->meta.start.abs_time)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - denom_h, - *denom_h); - return GNUNET_SYSERR; - } - - if (dk->recoup_possible) - { - SET_ERROR (mc, - MELT_ERROR_DENOMINATION_REVOKED); - return GNUNET_SYSERR; - } - - /* In case of age melt, make sure that the denomination supports age restriction */ - if (! (mc->request.refresh.coin.no_age_commitment) && - (0 == dk->denom_pub.age_mask.bits)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - denom_h, - *denom_h); - return GNUNET_SYSERR; - } - if ((mc->request.refresh.coin.no_age_commitment) && - (0 != dk->denom_pub.age_mask.bits)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID); - return GNUNET_SYSERR; - } - - return GNUNET_OK; -} - - -/** - * Check if the given array of hashes of denomination_keys - * - belong to valid denominations - * - calculate the total amount of the denominations including fees - * for melt. - * - * @param mc context of the melt to check keys for - */ -static void -phase_check_keys ( - struct MeltContext *mc) -{ - bool is_cs_denom[mc->request.refresh.num_coins]; - - memset (is_cs_denom, - 0, - sizeof(is_cs_denom)); - - mc->request.refresh.denom_serials = - GNUNET_new_array (mc->request.refresh.num_coins, - uint64_t); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &mc->request.amount)); - - /* Calculate the total value and withdraw fees for the fresh coins */ - for (size_t i = 0; i < mc->request.refresh.num_coins; i++) - { - struct TEH_DenominationKey *dk; - - if (GNUNET_OK != - find_denomination (mc, - &mc->request.denoms_h[i], - &dk)) - return; - - if (GNUNET_CRYPTO_BSA_CS == - dk->denom_pub.bsign_pub_key->cipher) - { - if (mc->request.refresh.no_blinding_seed) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_BLINDING_SEED_REQUIRED); - return; - } - mc->request.refresh.num_cs_r_values++; - is_cs_denom[i] = true; - } - /* 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 != - mc->request.planchets[k][i].blinded_message->cipher) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_COIN_CIPHER_MISMATCH); - return; - } - } - } - /* Accumulate the values */ - if (0 > TALER_amount_add (&mc->request.amount, - &mc->request.amount, - &dk->meta.value)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AMOUNT_OVERFLOW); - return; - } - /* Accumulate the withdraw fees for the fresh coins */ - if (0 > TALER_amount_add (&mc->request.amount, - &mc->request.amount, - &dk->meta.fees.withdraw)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); - return; - } - mc->request.refresh.denom_serials[i] = dk->meta.serial; - } - - /** - * Calculate the amount (with withdraw fee) plus refresh fee and - * compare with the value provided by the client in the request. - */ - { - struct TALER_Amount amount_with_fee; - - if (0 > TALER_amount_add (&amount_with_fee, - &mc->request.amount, - &mc->melted_coin_denom->meta.fees.refresh)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); - return; - } - - if (0 != TALER_amount_cmp (&amount_with_fee, - &mc->request.refresh.amount_with_fee)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT); - return; - } - } - - /* Save the indices of CS denominations */ - if (0 < mc->request.refresh.num_cs_r_values) - { - size_t j = 0; - - mc->request.cs_indices = GNUNET_new_array ( - mc->request.refresh.num_cs_r_values, - uint32_t); - - for (size_t i = 0; i < mc->request.refresh.num_coins; i++) - { - if (is_cs_denom[i]) - mc->request.cs_indices[j++] = i; - } - } - mc->phase++; -} - - -/** - * Check that the client signature authorizing the melt is valid. - * - * @param[in,out] mc request context to check - */ -static void -phase_check_coin_signature ( - struct MeltContext *mc) -{ - /* We can now compute the commitment */ - { - struct TALER_KappaHashBlindedPlanchetsP k_bps_h = {0}; - struct TALER_KappaTransferPublicKeys k_transfer_pubs = {0}; - - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - TALER_wallet_blinded_planchets_hash ( - mc->request.refresh.num_coins, - mc->request.planchets[k], - mc->request.denoms_h, - &k_bps_h.tuple[k]); - - if (! mc->request.refresh.is_v27_refresh) - { - k_transfer_pubs.num_transfer_pubs = mc->request.refresh.num_coins; - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - k_transfer_pubs.batch[k] = &mc->request.refresh.transfer_pubs[k]; - } - - TALER_refresh_get_commitment ( - &mc->request.refresh.rc, - &mc->request.refresh.refresh_seed, - mc->request.no_blinding_seed - ? NULL - : &mc->request.refresh.blinding_seed, - mc->request.refresh.is_v27_refresh - ? NULL - : &k_transfer_pubs, - &k_bps_h, - &mc->request.refresh.coin.coin_pub, - &mc->request.refresh.amount_with_fee); - } - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_melt_verify ( - &mc->request.refresh.amount_with_fee, - &mc->melted_coin_denom->meta.fees.refresh, - &mc->request.refresh.rc, - &mc->request.refresh.coin.denom_pub_hash, - &mc->request.refresh.coin.h_age_commitment, - &mc->request.refresh.coin.coin_pub, - &mc->request.refresh.coin_sig)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_COIN_SIGNATURE_INVALID); - return; - } - - mc->phase++; -} - - -/** - * Check for information about the melted coin's denomination, - * extracting its validity status and fee structure. - * Baseline: check if deposits/refreshes are generally - * simply still allowed for this denomination. - * - * @param mc parsed request information - */ -static void -phase_check_melt_valid (struct MeltContext *mc) -{ - enum MeltPhase current_phase = mc->phase; - /** - * Find the old coin's denomination. - * Note that we return only on GNUNET_SYSERR, - * because GNUNET_NO for the expired denomination - * will be handled below, with the zombie-check. - */ - if (GNUNET_SYSERR == - find_denomination (mc, - &mc->request.refresh.coin.denom_pub_hash, - &mc->melted_coin_denom)) - return; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Melted coin's denomination is worth %s\n", - TALER_amount2s (&mc->melted_coin_denom->meta.value)); - - /* sanity-check that "total melt amount > melt fee" */ - if (0 < - TALER_amount_cmp (&mc->melted_coin_denom->meta.fees.refresh, - &mc->request.refresh.amount_with_fee)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_FEES_EXCEED_CONTRIBUTION); - return; - } - - if (GNUNET_OK != - TALER_test_coin_valid (&mc->request.refresh.coin, - &mc->melted_coin_denom->denom_pub)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_DENOMINATION_SIGNATURE_INVALID); - return; - } - - /** - * find_denomination might have set the phase to - * produce an error, but we are still investigating. - * We reset the phase. - */ - mc->phase = current_phase; - mc->error.code = MELT_ERROR_NONE; - - if (GNUNET_TIME_absolute_is_past ( - mc->melted_coin_denom->meta.expire_deposit.abs_time)) - { - /** - * We are past deposit expiration time, but maybe this is a zombie? - */ - struct TALER_DenominationHashP denom_hash; - enum GNUNET_DB_QueryStatus qs; - - /* Check that the coin is dirty (we have seen it before), as we will - not just allow melting of a *fresh* coin where the denomination was - revoked (those must be recouped) */ - qs = TEH_plugin->get_coin_denomination ( - TEH_plugin->cls, - &mc->request.refresh.coin.coin_pub, - &mc->known_coin_id, - &denom_hash); - if (0 > qs) - { - /* There is no good reason for a serialization failure here: */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - SET_ERROR (mc, - MELT_ERROR_DB_FETCH_FAILED); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - /* We never saw this coin before, so _this_ justification is not OK. - * Note that the error was already set in find_denominations. */ - GNUNET_assert (MELT_ERROR_DENOMINATION_EXPIRED == - mc->error.code); - GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == - mc->phase); - return; - } - /* sanity check */ - if (0 != - GNUNET_memcmp (&denom_hash, - &mc->request.refresh.coin.denom_pub_hash)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_COIN_CONFLICTING_DENOMINATION_KEY, - denom_h, - denom_hash); - return; - } - /* Minor optimization: no need to run the - "ensure_coin_known" part of the transaction */ - mc->coin_is_known = true; - /* check later that zombie is satisfied */ - mc->zombie_required = true; - } - mc->phase++; -} - - -/** - * The request for melt was parsed successfully. - * Sign and persist the chosen blinded coins for the reveal step. - * - * @param mc The context for the current melt request - */ -static void -phase_prepare_transaction ( - struct MeltContext *mc) -{ - mc->request.refresh.denom_sigs - = GNUNET_new_array ( - mc->request.refresh.num_coins, - struct TALER_BlindedDenominationSignature); - mc->request.refresh.noreveal_index = - GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, - TALER_CNC_KAPPA); - - /* Choose and sign the coins */ - { - struct TEH_CoinSignData csds[mc->request.refresh.num_coins]; - enum TALER_ErrorCode ec_denomination_sign; - size_t noreveal_idx = mc->request.refresh.noreveal_index; - - memset (csds, - 0, - sizeof(csds)); - - /* Pick the chosen blinded coins */ - for (size_t i = 0; i<mc->request.refresh.num_coins; i++) - { - csds[i].bp = &mc->request.planchets[noreveal_idx][i]; - csds[i].h_denom_pub = &mc->request.denoms_h[i]; - } - - ec_denomination_sign = TEH_keys_denomination_batch_sign ( - mc->request.refresh.num_coins, - csds, - true, /* for melt */ - mc->request.refresh.denom_sigs); - if (TALER_EC_NONE != ec_denomination_sign) - { - GNUNET_break (0); - SET_ERROR_WITH_FIELD (mc, - MELT_ERROR_DENOMINATION_SIGN, - ec_denomination_sign); - return; - } - - /* Save the hash of chosen planchets */ - mc->request.refresh.selected_h = - mc->request.kappa_planchets_h.tuple[noreveal_idx]; - - /* If applicable, save the chosen transfer public keys */ - if (! mc->request.refresh.is_v27_refresh) - mc->request.refresh.transfer_pubs = - mc->request.transfer_pubs[noreveal_idx]; - - /** - * For the denominations with cipher CS, calculate the R-values - * and save the choices we made now, as at a later point, the - * private keys for the denominations might now be available anymore - * to make the same choice again. - */ - if (0 < mc->request.refresh.num_cs_r_values) - { - size_t num_cs_r_values = mc->request.refresh.num_cs_r_values; - struct TEH_CsDeriveData cdds[num_cs_r_values]; - struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_values]; - - memset (nonces, - 0, - sizeof(nonces)); - mc->request.refresh.cs_r_values = - GNUNET_new_array (num_cs_r_values, - struct GNUNET_CRYPTO_CSPublicRPairP); - mc->request.refresh.cs_r_choices = 0; - - GNUNET_assert (! mc->request.refresh.no_blinding_seed); - TALER_cs_derive_nonces_from_seed ( - &mc->request.refresh.blinding_seed, - true, /* for melt */ - num_cs_r_values, - mc->request.cs_indices, - nonces); - - for (size_t i = 0; i < num_cs_r_values; i++) - { - size_t idx = mc->request.cs_indices[i]; - - GNUNET_assert (idx < mc->request.refresh.num_coins); - cdds[i].h_denom_pub = &mc->request.denoms_h[idx]; - cdds[i].nonce = &nonces[i]; - } - - /** - * Let the crypto helper generate the R-values and - * make the choices - */ - if (TALER_EC_NONE != - TEH_keys_denomination_cs_batch_r_pub_simple ( - mc->request.refresh.num_cs_r_values, - cdds, - true, /* for melt */ - mc->request.refresh.cs_r_values)) - { - GNUNET_break (0); - SET_ERROR (mc, - MELT_ERROR_CRYPTO_HELPER); - return; - } - - /* Now save the choices for the selected bits */ - GNUNET_assert (num_cs_r_values <= 64); - for (size_t i = 0; i < num_cs_r_values; i++) - { - size_t idx = mc->request.cs_indices[i]; - - struct TALER_BlindedDenominationSignature *sig = - &mc->request.refresh.denom_sigs[idx]; - uint64_t bit = sig->blinded_sig->details.blinded_cs_answer.b; - - mc->request.refresh.cs_r_choices |= bit << i; - GNUNET_static_assert ( - TALER_MAX_COINS <= - sizeof(mc->request.refresh.cs_r_choices) * 8); - } - } - } - mc->phase++; -} - - -/** - * Generates response for the melt request. - * - * @param mc melt operation context - */ -static void -phase_generate_reply_success (struct MeltContext *mc) -{ - struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *db_obj; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec_confirmation_sign; - - db_obj = mc->request.is_idempotent - ? &mc->request.refresh_idem - : &mc->request.refresh; - ec_confirmation_sign = - TALER_exchange_online_melt_confirmation_sign ( - &TEH_keys_exchange_sign_, - &db_obj->rc, - db_obj->noreveal_index, - &pub, - &sig); - if (TALER_EC_NONE != ec_confirmation_sign) - { - SET_ERROR_WITH_FIELD (mc, - MELT_ERROR_CONFIRMATION_SIGN, - ec_confirmation_sign); - return; - } - - finish_loop (mc, - TALER_MHD_REPLY_JSON_PACK ( - mc->rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("noreveal_index", - db_obj->noreveal_index), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub))); -} - - -/** - * Check if the melt request is replayed and we already have an answer. - * If so, replay the existing answer and return the HTTP response. - * - * @param[in,out] mc parsed request data - * @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 -melt_is_idempotent ( - struct MeltContext *mc) -{ - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_refresh ( - TEH_plugin->cls, - &mc->request.refresh.rc, - &mc->request.refresh_idem); - if (0 > qs) - { - /* FIXME: soft error not handled correctly! */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DB_FETCH_FAILED, - db_fetch_context, - "get_refresh"); - return true; /* Well, kind-of. */ - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return false; - - mc->request.is_idempotent = true; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "request is idempotent\n"); - - /* Generate idempotent reply */ - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_MELT]++; - mc->phase = MELT_PHASE_GENERATE_REPLY_SUCCESS; - mc->error.code = MELT_ERROR_NONE; - return true; -} - - -/** - * Reports an error, potentially with details. - * That is, it puts a error-type specific response into the MHD queue. - * It will do a idempotency check first, if needed for the error type. - * - * @param mc melt context - */ -static void -phase_generate_reply_error ( - struct MeltContext *mc) -{ - GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == mc->phase); - GNUNET_assert (MELT_ERROR_NONE != mc->error.code); - - if (IDEMPOTENCY_CHECK_REQUIRED (mc->error.code) && - melt_is_idempotent (mc)) - { - return; - } - - switch (mc->error.code) - { - case MELT_ERROR_NONE: - break; - case MELT_ERROR_REQUEST_PARAMETER_MALFORMED: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - mc->error.details.request_parameter_malformed)); - return; - case MELT_ERROR_KEYS_MISSING: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL)); - return; - case MELT_ERROR_DB_FETCH_FAILED: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - mc->error.details.db_fetch_context)); - return; - case MELT_ERROR_DB_INVARIANT_FAILURE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL)); - return; - case MELT_ERROR_DB_PREFLIGHT_FAILURE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "make_coin_known")); - return; - case MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - "preflight failure")); - return; - case MELT_ERROR_COIN_UNKNOWN: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, - NULL)); - return; - case MELT_COIN_CONFLICTING_DENOMINATION_KEY: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, - TALER_B2S (&mc->error.details.denom_h))); - return; - case MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, - NULL)); - return; - case MELT_ERROR_DENOMINATION_SIGN: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - mc->error.details.ec_denomination_sign, - NULL)); - return; - case MELT_ERROR_DENOMINATION_SIGNATURE_INVALID: - finish_loop (mc, - TALER_MHD_reply_with_error (mc->rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL)); - return; - case MELT_ERROR_DENOMINATION_KEY_UNKNOWN: - GNUNET_break_op (0); - finish_loop (mc, - TEH_RESPONSE_reply_unknown_denom_pub_hash ( - mc->rc->connection, - &mc->error.details.denom_h)); - return; - case MELT_ERROR_DENOMINATION_EXPIRED: - GNUNET_break_op (0); - finish_loop (mc, - TEH_RESPONSE_reply_expired_denom_pub_hash ( - mc->rc->connection, - &mc->error.details.denom_h, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "MELT")); - return; - case MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: - finish_loop (mc, - TEH_RESPONSE_reply_expired_denom_pub_hash ( - mc->rc->connection, - &mc->error.details.denom_h, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "MELT")); - return; - case MELT_ERROR_DENOMINATION_REVOKED: - GNUNET_break_op (0); - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - NULL)); - return; - case MELT_ERROR_COIN_CIPHER_MISMATCH: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL)); - return; - case MELT_ERROR_BLINDING_SEED_REQUIRED: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_GENERIC_PARAMETER_MISSING, - "blinding_seed")); - return; - case MELT_ERROR_CRYPTO_HELPER: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL)); - return; - case MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION: - { - char msg[256]; - - GNUNET_snprintf (msg, - sizeof(msg), - "denomination %s does not support age restriction", - GNUNET_h2s (&mc->error.details.denom_h.hash)); - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, - msg)); - return; - } - case MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, - "old_age_commitment_h")); - return; - case MELT_ERROR_AMOUNT_OVERFLOW: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, - "amount")); - return; - case MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, - "amount+fee")); - return; - case MELT_ERROR_FEES_EXCEED_CONTRIBUTION: - finish_loop (mc, - TALER_MHD_reply_with_error (mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, - NULL)); - return; - case MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, - "value_with_fee incorrect")); - return; - case MELT_ERROR_CONFIRMATION_SIGN: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - mc->error.details.ec_confirmation_sign, - NULL)); - return; - case MELT_ERROR_INSUFFICIENT_FUNDS: - finish_loop (mc, - TEH_RESPONSE_reply_coin_insufficient_funds ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &mc->request.refresh.coin.denom_pub_hash, - &mc->request.refresh.coin.coin_pub)); - return; - case MELT_ERROR_DUPLICATE_PLANCHET: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error! */ - "duplicate planchet")); - return; - case MELT_ERROR_DUPLICATE_TRANSFER_PUB: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error! */ - "duplicate transfer_pub")); - return; - case MELT_ERROR_NONCE_RESUSE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error */ - "nonce reuse")); - return; - case MELT_ERROR_COIN_SIGNATURE_INVALID: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_MELT_COIN_SIGNATURE_INVALID, - NULL)); - return; - } - GNUNET_break (0); - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "error phase without error")); -} - - -/** - * Function implementing melt 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 MeltContext *` - * @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 -melt_transaction ( - void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct MeltContext *mc = cls; - enum GNUNET_DB_QueryStatus qs; - bool balance_ok; - bool found; - bool nonce_reuse; - uint32_t noreveal_index; - struct TALER_Amount insufficient_funds; - - (void) connection; - (void) mhd_ret; - - qs = TEH_plugin->do_refresh (TEH_plugin->cls, - &mc->request.refresh, - &mc->now, - &found, - &noreveal_index, - &mc->zombie_required, - &nonce_reuse, - &balance_ok, - &insufficient_funds); - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DB_FETCH_FAILED, - db_fetch_context, - "do_refresh"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_COIN_UNKNOWN); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (found) - { - /** - * This request is idempotent, set the nonreveal_index - * to the previous one and reply success. - */ - mc->request.refresh.noreveal_index = noreveal_index; - mc->phase = MELT_PHASE_GENERATE_REPLY_SUCCESS; - mc->error.code = MELT_ERROR_NONE; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - } - if (nonce_reuse) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_NONCE_RESUSE); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - GNUNET_break_op (0); - SET_ERROR_WITH_FIELD (mc, - MELT_ERROR_INSUFFICIENT_FUNDS, - insufficient_funds); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (mc->zombie_required) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT]++; - return qs; -} - - -/** - * The request was prepared successfully. - * Run the main DB transaction. - * - * @param mc The context for the current melt request - */ -static void -phase_run_transaction ( - struct MeltContext *mc) -{ - if (GNUNET_SYSERR == - TEH_plugin->preflight (TEH_plugin->cls)) - { - GNUNET_break (0); - SET_ERROR (mc, - MELT_ERROR_DB_PREFLIGHT_FAILURE); - return; - } - - /* first, make sure coin is known */ - if (! mc->coin_is_known) - { - MHD_RESULT mhd_ret = -1; - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++) - { - qs = TEH_make_coin_known (&mc->request.refresh.coin, - mc->rc->connection, - &mc->known_coin_id, - &mhd_ret); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - GNUNET_break (0); - /* Check if an answer has been queued */ - switch (mhd_ret) - { - case MHD_NO: - mc->phase = MELT_PHASE_RETURN_NO; - return; - case MHD_YES: - mc->phase = MELT_PHASE_RETURN_YES; - return; - default: /* ignore */ - ; - } - SET_ERROR (mc, - MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE); - return; - } - } - - /* run main database transaction */ - { - MHD_RESULT mhd_ret = -1; - enum GNUNET_GenericReturnValue ret; - enum MeltPhase current_phase = mc->phase; - - GNUNET_assert (MELT_PHASE_RUN_TRANSACTION == - current_phase); - ret = TEH_DB_run_transaction (mc->rc->connection, - "run melt", - TEH_MT_REQUEST_MELT, - &mhd_ret, - &melt_transaction, - mc); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - /* Check if an answer has been queued */ - switch (mhd_ret) - { - case MHD_NO: - mc->phase = MELT_PHASE_RETURN_NO; - return; - case MHD_YES: - mc->phase = MELT_PHASE_RETURN_YES; - return; - default: /* ignore */ - ; - } - GNUNET_assert (MELT_ERROR_NONE != mc->error.code); - GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == mc->phase); - return; - } - /** - * In case of idempotency (which is not an error condition), - * the phase has changed in melt_transaction. - * We simple return. - */ - if (current_phase != mc->phase) - return; - } - mc->phase++; -} - - -MHD_RESULT -TEH_handler_melt ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]) -{ - struct MeltContext *mc = rc->rh_ctx; - - (void) args; - if (NULL == mc) - { - mc = GNUNET_new (struct MeltContext); - rc->rh_ctx = mc; - rc->rh_cleaner = &clean_melt_rc; - mc->rc = rc; - mc->now = GNUNET_TIME_timestamp_get (); - } - - while (true) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "melt processing in phase %d\n", - mc->phase); - switch (mc->phase) - { - case MELT_PHASE_PARSE: - phase_parse_request (mc, - root); - break; - case MELT_PHASE_CHECK_MELT_VALID: - phase_check_melt_valid (mc); - break; - case MELT_PHASE_CHECK_KEYS: - phase_check_keys (mc); - break; - case MELT_PHASE_CHECK_COIN_SIGNATURE: - phase_check_coin_signature (mc); - break; - case MELT_PHASE_PREPARE_TRANSACTION: - phase_prepare_transaction (mc); - break; - case MELT_PHASE_RUN_TRANSACTION: - phase_run_transaction (mc); - break; - case MELT_PHASE_GENERATE_REPLY_SUCCESS: - phase_generate_reply_success (mc); - break; - case MELT_PHASE_GENERATE_REPLY_ERROR: - phase_generate_reply_error (mc); - break; - case MELT_PHASE_RETURN_YES: - return MHD_YES; - case MELT_PHASE_RETURN_NO: - return MHD_NO; - } - } -} diff --git a/src/exchange/taler-exchange-httpd_melt.h b/src/exchange/taler-exchange-httpd_melt.h @@ -1,52 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_melt.h - * @brief Handle /melt requests, starting with vDOLDPLUS of the protocol - * @author Özgür Kesim - */ -#ifndef TALER_EXCHANGE_HTTPD_MELT_H -#define TALER_EXCHANGE_HTTPD_MELT_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - -/** - * Resume suspended connections, we are shutting down. - */ -void -TEH_melt_cleanup (void); - - -/** - * Handle a "/melt" request. Parses the request into the JSON - * components and validates the melted coins, the signature and - * execute the melt as database transaction. - * - * @param rc the request context - * @param root uploaded JSON data - * @param args array of additional options, not used - * @return MHD result code - */ -MHD_RESULT -TEH_handler_melt ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_metrics.c b/src/exchange/taler-exchange-httpd_metrics.c @@ -1,160 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2015-2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_metrics.c - * @brief Handle /metrics requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_json_lib.h> -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_metrics.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include <jansson.h> - - -unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; - -unsigned long long TEH_METRICS_withdraw_num_coins; - -unsigned long long TEH_METRICS_num_conflict[TEH_MT_REQUEST_COUNT]; - -unsigned long long TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_COUNT]; - -unsigned long long TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_COUNT]; - -unsigned long long TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_COUNT]; - -unsigned long long TEH_METRICS_num_success[TEH_MT_SUCCESS_COUNT]; - - -MHD_RESULT -TEH_handler_metrics (struct TEH_RequestContext *rc, - const char *const args[]) -{ - char *reply; - struct MHD_Response *resp; - MHD_RESULT ret; - - (void) args; - GNUNET_asprintf (&reply, - "taler_exchange_success_transactions{type=\"%s\"} %llu\n" - "taler_exchange_success_transactions{type=\"%s\"} %llu\n" - "taler_exchange_success_transactions{type=\"%s\"} %llu\n" - "taler_exchange_success_transactions{type=\"%s\"} %llu\n" - "# HELP taler_exchange_serialization_failures " - " number of database serialization errors by type\n" - "# TYPE taler_exchange_serialization_failures counter\n" - "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" - "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" - "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" - "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" - "# HELP taler_exchange_received_requests " - " number of received requests by type\n" - "# TYPE taler_exchange_received_requests counter\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" -#if NOT_YET_IMPLEMENTED - "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" - "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" -#endif - "# HELP taler_exchange_num_signatures " - " number of signatures created by cipher\n" - "# TYPE taler_exchange_num_signatures counter\n" - "taler_exchange_num_signatures{type=\"%s\"} %llu\n" - "taler_exchange_num_signatures{type=\"%s\"} %llu\n" - "taler_exchange_num_signatures{type=\"%s\"} %llu\n" - "# HELP taler_exchange_num_signature_verifications " - " number of signatures verified by cipher\n" - "# TYPE taler_exchange_num_signature_verifications counter\n" - "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" - "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" - "taler_exchange_num_signature_verifications{type=\"%s\"} %llu\n" - "# HELP taler_exchange_num_keyexchanges " - " number of key exchanges done by cipher\n" - "# TYPE taler_exchange_num_keyexchanges counter\n" - "taler_exchange_num_keyexchanges{type=\"%s\"} %llu\n" - "# HELP taler_exchange_withdraw_num_coins " - " number of coins withdrawn in a withdraw request\n" - "# TYPE taler_exchange_withdraw_num_coins counter\n" - "taler_exchange_withdraw_num_coins{} %llu\n", - "deposit", - TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT], - "withdraw", - TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW], - "melt", - TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT], - "refresh-reveal", - TEH_METRICS_num_success[TEH_MT_SUCCESS_REFRESH_REVEAL], - "other", - TEH_METRICS_num_conflict[TEH_MT_REQUEST_OTHER], - "deposit", - TEH_METRICS_num_conflict[TEH_MT_REQUEST_DEPOSIT], - "withdraw", - TEH_METRICS_num_conflict[TEH_MT_REQUEST_WITHDRAW], - "melt", - TEH_METRICS_num_conflict[TEH_MT_REQUEST_MELT], - "other", - TEH_METRICS_num_requests[TEH_MT_REQUEST_OTHER], - "deposit", - TEH_METRICS_num_requests[TEH_MT_REQUEST_DEPOSIT], - "withdraw", - TEH_METRICS_num_requests[TEH_MT_REQUEST_WITHDRAW], - "melt", - TEH_METRICS_num_requests[TEH_MT_REQUEST_MELT], - "withdraw", - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW], -#if NOT_YET_IMPLEMENTED - "deposit", - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT], - "melt", - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_MELT], - "kyc-upload", - TEH_METRICS_num_requests[TEH_MT_REQUEST_KYC_UPLOAD], -#endif - "rsa", - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA], - "cs", - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_CS], - "eddsa", - TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_EDDSA], - "rsa", - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA], - "cs", - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS], - "eddsa", - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA], - "ecdh", - TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_ECDH], - TEH_METRICS_withdraw_num_coins); - resp = MHD_create_response_from_buffer (strlen (reply), - reply, - MHD_RESPMEM_MUST_FREE); - ret = MHD_queue_response (rc->connection, - MHD_HTTP_OK, - resp); - MHD_destroy_response (resp); - return ret; -} - - -/* end of taler-exchange-httpd_metrics.c */ diff --git a/src/exchange/taler-exchange-httpd_metrics.h b/src/exchange/taler-exchange-httpd_metrics.h @@ -1,132 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014--2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_metrics.h - * @brief Handle /metrics requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_METRICS_H -#define TALER_EXCHANGE_HTTPD_METRICS_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Request types for which we collect metrics. - */ -enum TEH_MetricTypeRequest -{ - TEH_MT_REQUEST_OTHER = 0, - TEH_MT_REQUEST_DEPOSIT = 1, - TEH_MT_REQUEST_WITHDRAW = 2, - TEH_MT_REQUEST_MELT = 3, - TEH_MT_REQUEST_PURSE_CREATE = 4, - TEH_MT_REQUEST_PURSE_MERGE = 5, - TEH_MT_REQUEST_RESERVE_PURSE = 6, - TEH_MT_REQUEST_PURSE_DEPOSIT = 7, - TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT = 8, - TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW = 9, - TEH_MT_REQUEST_IDEMPOTENT_MELT = 10, - TEH_MT_REQUEST_BATCH_DEPOSIT = 11, - TEH_MT_REQUEST_POLICY_FULFILLMENT = 12, - TEH_MT_REQUEST_KYC_UPLOAD = 13, - TEH_MT_REQUEST_COUNT = 14 /* MUST BE LAST! */ -}; - -/** - * Success types for which we collect metrics. - */ -enum TEH_MetricTypeSuccess -{ - TEH_MT_SUCCESS_DEPOSIT = 0, - TEH_MT_SUCCESS_WITHDRAW = 1, - TEH_MT_SUCCESS_MELT = 2, - TEH_MT_SUCCESS_REFRESH_REVEAL = 3, - TEH_MT_SUCCESS_WITHDRAW_REVEAL = 4, - TEH_MT_SUCCESS_COUNT = 5 /* MUST BE LAST! */ -}; - -/** - * Cipher types for which we collect signature metrics. - */ -enum TEH_MetricTypeSignature -{ - TEH_MT_SIGNATURE_RSA = 0, - TEH_MT_SIGNATURE_CS = 1, - TEH_MT_SIGNATURE_EDDSA = 2, - TEH_MT_SIGNATURE_COUNT = 3 -}; - -/** - * Cipher types for which we collect key exchange metrics. - */ -enum TEH_MetricTypeKeyX -{ - TEH_MT_KEYX_ECDH = 0, - TEH_MT_KEYX_COUNT = 1 -}; - -/** - * Number of requests handled of the respective type. - */ -extern unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; - -/** - * Number of successful requests handled of the respective type. - */ -extern unsigned long long TEH_METRICS_num_success[TEH_MT_SUCCESS_COUNT]; - -/** - * Number of coins withdrawn in a withdraw request - */ -extern unsigned long long TEH_METRICS_withdraw_num_coins; - -/** - * Number of serialization errors encountered when - * handling requests of the respective type. - */ -extern unsigned long long TEH_METRICS_num_conflict[TEH_MT_REQUEST_COUNT]; - -/** - * Number of signatures created by the respective cipher. - */ -extern unsigned long long TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_COUNT]; - -/** - * Number of signatures verified by the respective cipher. - */ -extern unsigned long long TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_COUNT]; - -/** - * Number of key exchanges done with the respective cipher. - */ -extern unsigned long long TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_COUNT]; - -/** - * Handle a "/metrics" request. - * - * @param rc request context - * @param args array of additional options (must be empty for this function) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_metrics (struct TEH_RequestContext *rc, - const char *const args[]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_post-aml-OFFICER_PUB-decision.c b/src/exchange/taler-exchange-httpd_post-aml-OFFICER_PUB-decision.c @@ -0,0 +1,535 @@ +/* + This file is part of TALER + Copyright (C) 2023-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-aml-OFFICER_PUB-decision.c + * @brief Handle POST request about an AML decision. + * @author Christian Grothoff + * @author Florian Dold + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h" + + +/** + * Context used for processing the AML decision request. + */ +struct AmlDecisionContext +{ + + /** + * Kept in a DLL. + */ + struct AmlDecisionContext *next; + + /** + * Kept in a DLL. + */ + struct AmlDecisionContext *prev; + + /** + * HTTP status code to use with @e response. + */ + unsigned int response_code; + + /** + * Response to return, NULL if none yet. + */ + struct MHD_Response *response; + + /** + * Request we are processing. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for async KYC processing. + */ + struct TEH_KycMeasureRunContext *kat; + +}; + +/** + * Kept in a DLL. + */ +static struct AmlDecisionContext *adc_head; + +/** + * Kept in a DLL. + */ +static struct AmlDecisionContext *adc_tail; + + +void +TEH_aml_decision_cleanup () +{ + struct AmlDecisionContext *adc; + + while (NULL != (adc = adc_head)) + { + MHD_resume_connection (adc->rc->connection); + GNUNET_CONTAINER_DLL_remove (adc_head, + adc_tail, + adc); + } +} + + +/** + * Function called to clean up aml decision context. + * + * @param[in,out] rc context to clean up + */ +static void +aml_decision_cleaner (struct TEH_RequestContext *rc) +{ + struct AmlDecisionContext *adc = rc->rh_ctx; + + if (NULL != adc->kat) + { + TEH_kyc_run_measure_cancel (adc->kat); + adc->kat = NULL; + } + if (NULL != adc->response) + { + MHD_destroy_response (adc->response); + adc->response = NULL; + } + GNUNET_free (adc); +} + + +/** + * Function called after the KYC-AML trigger is done. + * + * @param cls closure + * @param ec error code or 0 on success + * @param detail error message or NULL on success / no info + */ +static void +aml_trigger_callback ( + void *cls, + enum TALER_ErrorCode ec, + const char *detail) +{ + struct AmlDecisionContext *adc = cls; + + adc->kat = NULL; + GNUNET_assert (NULL == adc->response); + + if (TALER_EC_NONE != ec) + { + adc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + adc->response = TALER_MHD_make_error ( + ec, + detail); + } + else + { + adc->response_code = MHD_HTTP_NO_CONTENT; + adc->response = MHD_create_response_from_buffer_static ( + 0, + ""); + TALER_MHD_add_global_headers (adc->response, + true); + } + + GNUNET_assert (NULL != adc->response); + + MHD_resume_connection (adc->rc->connection); + GNUNET_CONTAINER_DLL_remove (adc_head, + adc_tail, + adc); + TALER_MHD_daemon_trigger (); +} + + +MHD_RESULT +TEH_handler_post_aml_decision ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const json_t *root) +{ + struct MHD_Connection *connection = rc->connection; + struct AmlDecisionContext *adc = rc->rh_ctx; + const char *justification; + const char *new_measures = NULL; + bool to_investigate; + struct GNUNET_TIME_Timestamp decision_time; + struct GNUNET_TIME_Timestamp attributes_expiration + = GNUNET_TIME_UNIT_ZERO_TS; + const json_t *new_rules; + const json_t *events = NULL; + const json_t *properties = NULL; + const json_t *attributes = NULL; + struct TALER_FullPayto payto_uri = { + .full_payto = NULL + }; + struct TALER_NormalizedPaytoHashP h_payto; + struct TALER_AmlOfficerSignatureP officer_sig; + struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs = NULL; + uint64_t legi_measure_serial_id = 0; + MHD_RESULT ret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ( + "new_measures", + &new_measures), + NULL), + GNUNET_JSON_spec_string ("justification", + &justification), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_full_payto_uri ("payto_uri", + &payto_uri), + NULL), + GNUNET_JSON_spec_fixed_auto ("h_payto", + &h_payto), + GNUNET_JSON_spec_object_const ("new_rules", + &new_rules), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("properties", + &properties), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("events", + &events), + NULL), + GNUNET_JSON_spec_bool ("keep_investigating", + &to_investigate), + GNUNET_JSON_spec_fixed_auto ("officer_sig", + &officer_sig), + GNUNET_JSON_spec_timestamp ("decision_time", + &decision_time), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("attributes_expiration", + &attributes_expiration), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("attributes", + &attributes), + NULL), + GNUNET_JSON_spec_end () + }; + struct GNUNET_TIME_Timestamp expiration_time; + json_t *jmeasures = NULL; + bool is_wallet; + size_t eas = 0; + void *ea = NULL; + + if (NULL == adc) + { + /* Initialize context */ + adc = GNUNET_new (struct AmlDecisionContext); + adc->rc = rc; + rc->rh_ctx = adc; + rc->rh_cleaner = aml_decision_cleaner; + } + + if (NULL != adc->response) + { + ret = MHD_queue_response (rc->connection, + adc->response_code, + adc->response); + goto done; + } + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + ret = MHD_NO; /* hard failure */ + goto done; + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + ret = MHD_YES /* failure */; + goto done; + } + if ( (NULL != attributes) && + (! json_is_string (json_object_get (attributes, + "FORM_ID"))) ) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "attributes['FORM_ID']"); + goto done; + } + } + if (NULL != payto_uri.full_payto) + { + struct TALER_NormalizedPaytoHashP h_payto2; + + TALER_full_payto_normalize_and_hash (payto_uri, + &h_payto2); + if (0 != + GNUNET_memcmp (&h_payto, + &h_payto2)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + goto done; + } + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_officer_aml_decision_verify ( + justification, + decision_time, + &h_payto, + new_rules, + properties, + new_measures, + to_investigate, + officer_pub, + &officer_sig, + attributes_expiration, + attributes)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID, + NULL); + goto done; + } + + lrs = TALER_KYCLOGIC_rules_parse (new_rules); + if (NULL == lrs) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "legitimization rule malformed"); + goto done; + } + + expiration_time = TALER_KYCLOGIC_rules_get_expiration (lrs); + if ( (NULL != new_measures) && + (0 != strlen (new_measures)) ) + { + jmeasures + = TALER_KYCLOGIC_get_jmeasures (lrs, + new_measures); + if (NULL == jmeasures) + { + GNUNET_break_op (0); + /* Request specified a new_measure for which the given + rule set does not work as it does not define the measure */ + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "new_measures/new_rules"); + goto done; + } + } + + { + size_t num_events = json_array_size (events); + const char *sevents[GNUNET_NZL (num_events)]; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp last_date; + bool invalid_officer = true; + bool unknown_account = false; + struct GNUNET_HashCode h_attr = { 0 }; + const char *form_id = NULL; + + if (NULL != attributes) + { + form_id = json_string_value (json_object_get (attributes, + "FORM_ID")); + TALER_json_hash (attributes, + &h_attr); + TALER_CRYPTO_kyc_attributes_encrypt (&TEH_attribute_key, + attributes, + &ea, + &eas); + } + + for (size_t i = 0; i<num_events; i++) + { + sevents[i] = json_string_value (json_array_get (events, + i)); + if (NULL == sevents[i]) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "events"); + goto done; + } + } + /* We keep 'new_measures' around mostly so that + the auditor can later verify officer_sig */ + qs = TEH_plugin->insert_aml_decision (TEH_plugin->cls, + payto_uri, + &h_payto, + decision_time, + expiration_time, + properties, + new_rules, + to_investigate, + new_measures, + jmeasures, + justification, + officer_pub, + &officer_sig, + num_events, + sevents, + form_id, + eas, /* enc_attributes_size*/ + ea, /* enc_attributes*/ + NULL != attributes + ? &h_attr + : NULL, /* attributes_hash */ + attributes_expiration, + &invalid_officer, + &unknown_account, + &last_date, + &legi_measure_serial_id, + &is_wallet); + json_decref (jmeasures); + if (qs <= 0) + { + GNUNET_break (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_aml_decision"); + goto done; + } + if (invalid_officer) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_AML_DECISION_INVALID_OFFICER, + NULL); + goto done; + } + if (unknown_account) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_BANK_ACCOUNT_UNKNOWN, + "h_payto"); + goto done; + } + if (GNUNET_TIME_timestamp_cmp (last_date, + >, + decision_time)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT, + NULL); + goto done; + } + } + /* Run instant measure if necessary */ + { + const struct TALER_KYCLOGIC_Measure *instant_ms = NULL; + + if (NULL != new_measures) + { + instant_ms = TALER_KYCLOGIC_get_instant_measure (lrs, + new_measures); + } + + if (NULL != instant_ms) + { + /* We have an 'instant' measure which means we must run the + AML program immediately instead of waiting for the account owner + to select some measure and contribute their KYC data. */ + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running instant measure after AML decision\n"); + adc->kat = TEH_kyc_run_measure_directly ( + &rc->async_scope_id, + instant_ms, + &h_payto, + is_wallet, + &aml_trigger_callback, + adc + ); + if (NULL == adc->kat) + { + GNUNET_break (0); + ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG, + "TEH_kyc_finished"); + goto done; + } + MHD_suspend_connection (adc->rc->connection); + GNUNET_CONTAINER_DLL_insert (adc_head, + adc_tail, + adc); + ret = MHD_YES; + goto done; + } + } + ret = TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + goto done; + +done: + TALER_KYCLOGIC_rules_free (lrs); + GNUNET_free (ea); + return ret; +} + + +/* end of taler-exchange-httpd_aml-decision.c */ diff --git a/src/exchange/taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h b/src/exchange/taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h @@ -0,0 +1,67 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-aml-OFFICER_PUB-decision.h + * @brief Handle /aml/$OFFICER_PUB/decision requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_AML_OFFICER_PUB_DECISION_H +#define TALER_EXCHANGE_HTTPD_POST_AML_OFFICER_PUB_DECISION_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a POST "/aml/$OFFICER_PUB/decision" request. Parses the decision + * details, checks the signatures and if appropriately authorized executes + * the decision. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_post_aml_decision ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const json_t *root); + + +/** + * Handle a GET "/aml/$OFFICER_PUB/decisions" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching decisions. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be the state) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_decisions_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + +/** + * Clean up running POST /aml/$OFFICER_PUB/decisions requests. + */ +void +TEH_aml_decision_cleanup (void); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.c b/src/exchange/taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.c @@ -0,0 +1,232 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.c + * @brief Handle request to add auditor signature on a denomination. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for the #add_auditor_denom_sig transaction. + */ +struct AddAuditorDenomContext +{ + /** + * Auditor's signature affirming the AUDITORS XXX operation + * (includes timestamp). + */ + struct TALER_AuditorSignatureP auditor_sig; + + /** + * Denomination this is about. + */ + const struct TALER_DenominationHashP *h_denom_pub; + + /** + * Auditor this is about. + */ + const struct TALER_AuditorPublicKeyP *auditor_pub; + +}; + + +/** + * Function implementing database transaction to add an auditors. 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 closure with a `struct AddAuditorDenomContext` + * @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 +add_auditor_denom_sig (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AddAuditorDenomContext *awc = cls; + struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; + enum GNUNET_DB_QueryStatus qs; + char *auditor_url; + bool enabled; + + qs = TEH_plugin->lookup_denomination_key ( + TEH_plugin->cls, + awc->h_denom_pub, + &meta); + 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, + "lookup denomination key"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + GNUNET_h2s (&awc->h_denom_pub->hash)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->lookup_auditor_status ( + TEH_plugin->cls, + awc->auditor_pub, + &auditor_url, + &enabled); + 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, + "lookup auditor"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_PRECONDITION_FAILED, + TALER_EC_EXCHANGE_AUDITORS_AUDITOR_UNKNOWN, + TALER_B2S (awc->auditor_pub)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! enabled) + { + GNUNET_free (auditor_url); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_GONE, + TALER_EC_EXCHANGE_AUDITORS_AUDITOR_INACTIVE, + TALER_B2S (awc->auditor_pub)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_auditor_denom_validity_verify ( + auditor_url, + awc->h_denom_pub, + &TEH_master_public_key, + meta.start, + meta.expire_withdraw, + meta.expire_deposit, + meta.expire_legal, + &meta.value, + &meta.fees, + awc->auditor_pub, + &awc->auditor_sig)) + { + GNUNET_free (auditor_url); + /* signature invalid */ + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_free (auditor_url); + + qs = TEH_plugin->insert_auditor_denom_sig (TEH_plugin->cls, + awc->h_denom_pub, + awc->auditor_pub, + &awc->auditor_sig); + if (qs < 0) + { + GNUNET_break (0); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "add auditor signature"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_auditors ( + struct MHD_Connection *connection, + const struct TALER_AuditorPublicKeyP *auditor_pub, + const struct TALER_DenominationHashP *h_denom_pub, + const json_t *root) +{ + struct AddAuditorDenomContext awc = { + .auditor_pub = auditor_pub, + .h_denom_pub = h_denom_pub + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("auditor_sig", + &awc.auditor_sig), + GNUNET_JSON_spec_end () + }; + MHD_RESULT res; + enum GNUNET_GenericReturnValue ret; + + ret = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + ret = TEH_DB_run_transaction (connection, + "add auditor denom sig", + TEH_MT_REQUEST_OTHER, + &res, + &add_auditor_denom_sig, + &awc); + if (GNUNET_SYSERR == ret) + return res; + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_auditors.c */ diff --git a/src/exchange/taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.h b/src/exchange/taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-auditors-AUDITOR_PUB-H_DENOM_PUB.h + * @brief Handlers for the /auditors/ endpoints + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_AUDITORS_AUDITOR_PUB_H_DENOM_PUB_H +#define TALER_EXCHANGE_HTTPD_POST_AUDITORS_AUDITOR_PUB_H_DENOM_PUB_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/auditors/$AUDITOR_PUB/$H_DENOM_PUB" request. + * + * @param connection the MHD connection to handle + * @param root uploaded JSON data + * @param auditor_pub public key of the auditor + * @param h_denom_pub hash of the denomination public key + * @return MHD result code + */ +MHD_RESULT +TEH_handler_auditors ( + struct MHD_Connection *connection, + const struct TALER_AuditorPublicKeyP *auditor_pub, + const struct TALER_DenominationHashP *h_denom_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-batch-deposit.c b/src/exchange/taler-exchange-httpd_post-batch-deposit.c @@ -0,0 +1,1207 @@ +/* + This file is part of TALER + Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-batch-deposit.c + * @brief Handle /batch-deposit requests; parses the POST and JSON and + * verifies the coin signatures before handing things off + * to the database. + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_extensions_policy.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_post-batch-deposit.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_exchangedb_lib.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for #batch_deposit_transaction. + */ +struct BatchDepositContext +{ + + /** + * Kept in a DLL. + */ + struct BatchDepositContext *next; + + /** + * Kept in a DLL. + */ + struct BatchDepositContext *prev; + + /** + * The request we are working on. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for the legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Array with the individual coin deposit fees. + */ + struct TALER_Amount *deposit_fees; + + /** + * Information about deposited coins. + */ + struct TALER_EXCHANGEDB_CoinDepositInformation *cdis; + + /** + * Additional details for policy extension relevant for this + * deposit operation, possibly NULL! + */ + json_t *policy_json; + + /** + * Response to return, if set. + */ + struct MHD_Response *response; + + /** + * KYC status of the reserve used for the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Hash over @e policy_details, might be all zero + */ + struct TALER_ExtensionPolicyHashP h_policy; + + /** + * Hash over the merchant's payto://-URI with the wire salt. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * When @e policy_details are persisted, this contains the id of the record + * in the policy_details table. + */ + uint64_t policy_details_serial_id; + + /** + * Hash over the normalized payto://-URI of the account we are + * depositing into. + */ + struct TALER_NormalizedPaytoHashP nph; + + /** + * Our timestamp (when we received the request). + * Possibly updated by the transaction if the + * request is idempotent (was repeated). + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Total amount that is accumulated with this deposit, + * without fee. + */ + struct TALER_Amount accumulated_total_without_fee; + + /** + * Details about the batch deposit operation. + */ + struct TALER_EXCHANGEDB_BatchDeposit bd; + + /** + * If @e policy_json was present, the corresponding policy extension + * calculates these details. These will be persisted in the policy_details + * table. + */ + struct TALER_PolicyDetails policy_details; + + /** + * HTTP status to return with @e response, or 0. + */ + unsigned int http_status; + + /** + * Our current state in the state machine. + */ + enum + { + BDC_PHASE_INIT = 0, + BDC_PHASE_PARSE = 1, + BDC_PHASE_POLICY = 2, + BDC_PHASE_KYC = 3, + BDC_PHASE_TRANSACT = 4, + BDC_PHASE_REPLY_SUCCESS = 5, + BDC_PHASE_SUSPENDED, + BDC_PHASE_CHECK_KYC_RESULT, + BDC_PHASE_GENERATE_REPLY_FAILURE, + BDC_PHASE_RETURN_YES, + BDC_PHASE_RETURN_NO, + } phase; + + /** + * True, if no policy was present in the request. Then + * @e policy_json is NULL and @e h_policy will be all zero. + */ + bool has_no_policy; + + /** + * KYC failed because a KYC auth transfer is needed + * to establish the merchant_pub. + */ + bool bad_kyc_auth; +}; + + +/** + * Head of list of suspended batch deposit operations. + */ +static struct BatchDepositContext *bdc_head; + +/** + * Tail of list of suspended batch deposit operations. + */ +static struct BatchDepositContext *bdc_tail; + + +void +TEH_batch_deposit_cleanup () +{ + struct BatchDepositContext *bdc; + + while (NULL != (bdc = bdc_head)) + { + GNUNET_assert (BDC_PHASE_SUSPENDED == bdc->phase); + bdc->phase = BDC_PHASE_RETURN_NO; + MHD_resume_connection (bdc->rc->connection); + GNUNET_CONTAINER_DLL_remove (bdc_head, + bdc_tail, + bdc); + } +} + + +/** + * Terminate the main loop by returning the final + * result. + * + * @param[in,out] bdc context to update phase for + * @param mres MHD status to return + */ +static void +finish_loop (struct BatchDepositContext *bdc, + MHD_RESULT mres) +{ + bdc->phase = (MHD_YES == mres) + ? BDC_PHASE_RETURN_YES + : BDC_PHASE_RETURN_NO; +} + + +/** + * Send confirmation of batch deposit success to client. This function will + * create a signed message affirming the given information and return it to + * the client. By this, the exchange affirms that the coins had sufficient + * (residual) value for the specified transaction and that it will execute the + * requested batch deposit operation with the given wiring details. + * + * @param[in,out] bdc information about the batch deposit + */ +static void +bdc_phase_reply_success ( + struct BatchDepositContext *bdc) +{ + const struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; + const struct TALER_CoinSpendSignatureP *csigs[GNUNET_NZL (bd->num_cdis)]; + enum TALER_ErrorCode ec; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) + csigs[i] = &bd->cdis[i].csig; + if (TALER_EC_NONE != + (ec = TALER_exchange_online_deposit_confirmation_sign ( + &TEH_keys_exchange_sign_, + &bd->h_contract_terms, + &bdc->h_wire, + bdc->has_no_policy ? NULL : &bdc->h_policy, + bdc->exchange_timestamp, + bd->wire_deadline, + bd->refund_deadline, + &bdc->accumulated_total_without_fee, + bd->num_cdis, + csigs, + &bdc->bd.merchant_pub, + &pub, + &sig))) + { + GNUNET_break (0); + finish_loop (bdc, + TALER_MHD_reply_with_ec (bdc->rc->connection, + ec, + NULL)); + return; + } + finish_loop (bdc, + TALER_MHD_REPLY_JSON_PACK ( + bdc->rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + bdc->exchange_timestamp), + TALER_JSON_pack_amount ("accumulated_total_without_fee", + &bdc->accumulated_total_without_fee), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig))); +} + + +/** + * Execute database transaction for /batch-deposit. 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 BatchDepositContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +batch_deposit_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct BatchDepositContext *bdc = cls; + const struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; + enum GNUNET_DB_QueryStatus qs = GNUNET_DB_STATUS_HARD_ERROR; + uint32_t bad_balance_coin_index = UINT32_MAX; + bool balance_ok; + bool in_conflict; + + /* If the deposit has a policy associated to it, persist it. This will + * insert or update the record. */ + if (! bdc->has_no_policy) + { + qs = TEH_plugin->persist_policy_details ( + TEH_plugin->cls, + &bdc->policy_details, + &bdc->bd.policy_details_serial_id, + &bdc->accumulated_total_without_fee, + &bdc->policy_details.fulfillment_state); + if (qs < 0) + return qs; + + bdc->bd.policy_blocked = + bdc->policy_details.fulfillment_state != TALER_PolicyFulfillmentSuccess; + } + + /* FIXME-#9373: replace by batch insert! */ + for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) + { + const struct TALER_EXCHANGEDB_CoinDepositInformation *cdi + = &bdc->cdis[i]; + uint64_t known_coin_id; + + qs = TEH_make_coin_known (&cdi->coin, + connection, + &known_coin_id, + mhd_ret); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "make coin known (%s) returned %d\n", + TALER_B2S (&cdi->coin.coin_pub), + qs); + if (qs < 0) + return qs; + } + qs = TEH_plugin->do_deposit ( + TEH_plugin->cls, + bd, + bdc->deposit_fees, + &bdc->exchange_timestamp, + &bdc->accumulated_total_without_fee, + &balance_ok, + &bad_balance_coin_index, + &in_conflict); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ( + "Failed to store /batch-deposit information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "batch-deposit"); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "do_deposit returned: %d / %s[%u] / %s\n", + qs, + balance_ok ? "balance ok" : "balance insufficient", + (unsigned int) bad_balance_coin_index, + in_conflict ? "in conflict" : "no conflict"); + if (in_conflict) + { + struct TALER_MerchantWireHashP h_wire; + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TEH_plugin->get_wire_hash_for_contract ( + TEH_plugin->cls, + &bd->merchant_pub, + &bd->h_contract_terms, + &h_wire)) + { + TALER_LOG_WARNING ( + "Failed to retrieve conflicting contract details from database\n"); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "batch-deposit"); + return qs; + } + + *mhd_ret + = TEH_RESPONSE_reply_coin_conflicting_contract ( + connection, + TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT, + &h_wire); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + GNUNET_assert (bad_balance_coin_index < bdc->bd.num_cdis); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "returning history of conflicting coin (%s)\n", + TALER_B2S (&bdc->cdis[bad_balance_coin_index].coin.coin_pub)); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &bdc->cdis[bad_balance_coin_index].coin.denom_pub_hash, + &bdc->cdis[bad_balance_coin_index].coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT]++; + return qs; +} + + +/** + * Run database transaction. + * + * @param[in,out] bdc request context + */ +static void +bdc_phase_transact (struct BatchDepositContext *bdc) +{ + MHD_RESULT mhd_ret; + + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure")); + return; + } + + if (GNUNET_OK != + TEH_DB_run_transaction (bdc->rc->connection, + "execute batch deposit", + TEH_MT_REQUEST_BATCH_DEPOSIT, + &mhd_ret, + &batch_deposit_transaction, + bdc)) + { + finish_loop (bdc, + mhd_ret); + return; + } + bdc->phase++; +} + + +/** + * Check if the @a bdc is replayed and we already have an + * answer. If so, replay the existing answer and return the + * HTTP response. + * + * @param bdc parsed request data + * @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 +check_request_idempotent ( + struct BatchDepositContext *bdc) +{ + const struct TEH_RequestContext *rc = bdc->rc; + enum GNUNET_DB_QueryStatus qs; + bool is_idempotent; + + qs = TEH_plugin->do_check_deposit_idempotent ( + TEH_plugin->cls, + &bdc->bd, + &bdc->exchange_timestamp, + &is_idempotent); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_check_deposit_idempotent")); + return true; + } + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (! is_idempotent) ) + return false; + bdc->phase = BDC_PHASE_REPLY_SUCCESS; + return true; +} + + +/** + * Check the KYC result. + * + * @param bdc storage for request processing + */ +static void +bdc_phase_check_kyc_result (struct BatchDepositContext *bdc) +{ + /* return final positive response */ + if ( (! bdc->kyc.ok) || + (bdc->bad_kyc_auth) ) + { + if (check_request_idempotent (bdc)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request is idempotent!\n"); + return; + } + /* KYC required */ + finish_loop (bdc, + TEH_RESPONSE_reply_kyc_required ( + bdc->rc->connection, + &bdc->nph, + &bdc->kyc, + bdc->bad_kyc_auth)); + return; + } + bdc->phase = BDC_PHASE_TRANSACT; +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +deposit_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct BatchDepositContext *bdc = cls; + + bdc->lch = NULL; + GNUNET_assert (BDC_PHASE_SUSPENDED == + bdc->phase); + MHD_resume_connection (bdc->rc->connection); + GNUNET_CONTAINER_DLL_remove (bdc_head, + bdc_tail, + bdc); + TALER_MHD_daemon_trigger (); + if (NULL != lcr->response) + { + bdc->response = lcr->response; + bdc->http_status = lcr->http_status; + bdc->phase = BDC_PHASE_GENERATE_REPLY_FAILURE; + return; + } + bdc->kyc = lcr->kyc; + bdc->bad_kyc_auth = lcr->bad_kyc_auth; + bdc->phase = BDC_PHASE_CHECK_KYC_RESULT; +} + + +/** + * 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 + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +deposit_amount_cb ( + void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct BatchDepositContext *bdc = cls; + enum GNUNET_GenericReturnValue ret; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signaling amount %s for KYC check during deposit\n", + TALER_amount2s (&bdc->accumulated_total_without_fee)); + ret = cb (cb_cls, + &bdc->accumulated_total_without_fee, + bdc->exchange_timestamp.abs_time); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + qs = TEH_plugin->select_deposit_amounts_for_kyc_check ( + TEH_plugin->cls, + &bdc->nph, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this deposit and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); + return qs; +} + + +/** + * Run KYC check. + * + * @param[in,out] bdc request context + */ +static void +bdc_phase_kyc (struct BatchDepositContext *bdc) +{ + if (GNUNET_YES != TEH_enable_kyc) + { + bdc->phase++; + return; + } + TALER_full_payto_normalize_and_hash (bdc->bd.receiver_wire_account, + &bdc->nph); + bdc->lch = TEH_legitimization_check2 ( + &bdc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, + bdc->bd.receiver_wire_account, + &bdc->nph, + &bdc->bd.merchant_pub, + &deposit_amount_cb, + bdc, + &deposit_legi_cb, + bdc); + GNUNET_assert (NULL != bdc->lch); + GNUNET_CONTAINER_DLL_insert (bdc_head, + bdc_tail, + bdc); + MHD_suspend_connection (bdc->rc->connection); + bdc->phase = BDC_PHASE_SUSPENDED; +} + + +/** + * Handle policy. + * + * @param[in,out] bdc request context + */ +static void +bdc_phase_policy (struct BatchDepositContext *bdc) +{ + const char *error_hint = NULL; + + if (bdc->has_no_policy) + { + bdc->phase++; + return; + } + if (GNUNET_OK != + TALER_extensions_create_policy_details ( + TEH_currency, + bdc->policy_json, + &bdc->policy_details, + &error_hint)) + { + GNUNET_break_op (0); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED, + error_hint)); + return; + } + + TALER_deposit_policy_hash (bdc->policy_json, + &bdc->h_policy); + bdc->phase++; +} + + +/** + * Parse per-coin deposit information from @a jcoin + * into @a deposit. Fill in generic information from + * @a ctx. + * + * @param bdc information about the overall batch + * @param jcoin coin data to parse + * @param[out] cdi where to store the result + * @param[out] deposit_fee where to write the deposit fee + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +static enum GNUNET_GenericReturnValue +parse_coin (const struct BatchDepositContext *bdc, + json_t *jcoin, + struct TALER_EXCHANGEDB_CoinDepositInformation *cdi, + struct TALER_Amount *deposit_fee) +{ + const struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("contribution", + TEH_currency, + &cdi->amount_with_fee), + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &cdi->coin.denom_pub_hash), + TALER_JSON_spec_denom_sig ("ub_sig", + &cdi->coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &cdi->coin.coin_pub), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &cdi->coin.h_age_commitment), + &cdi->coin.no_age_commitment), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &cdi->csig), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + if (GNUNET_OK != + (res = TALER_MHD_parse_json_data (bdc->rc->connection, + jcoin, + spec))) + return res; + /* check denomination exists and is valid */ + { + struct TEH_DenominationKey *dk; + MHD_RESULT mret; + + dk = TEH_keys_denomination_by_hash ( + &cdi->coin.denom_pub_hash, + bdc->rc->connection, + &mret); + if (NULL == dk) + { + GNUNET_JSON_parse_free (spec); + return (MHD_YES == mret) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (0 > TALER_amount_cmp (&dk->meta.value, + &cdi->amount_with_fee)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + { + /* This denomination is past the expiration time for deposits */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + bdc->rc->connection, + &cdi->coin.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "DEPOSIT")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + bdc->rc->connection, + &cdi->coin.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "DEPOSIT")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TEH_RESPONSE_reply_expired_denom_pub_hash ( + bdc->rc->connection, + &cdi->coin.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "DEPOSIT")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (dk->denom_pub.bsign_pub_key->cipher != + cdi->coin.denom_sig.unblinded_sig->cipher) + { + /* denomination cipher and denomination signature cipher not the same */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + *deposit_fee = dk->meta.fees.deposit; + /* check coin signature */ + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } + if (GNUNET_YES != + TALER_test_coin_valid (&cdi->coin, + &dk->denom_pub)) + { + TALER_LOG_WARNING ("Invalid coin passed for /batch-deposit\n"); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + } + if (0 < TALER_amount_cmp (deposit_fee, + &cdi->amount_with_fee)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_deposit_verify ( + &cdi->amount_with_fee, + deposit_fee, + &bdc->h_wire, + &bd->h_contract_terms, + &bd->wallet_data_hash, + cdi->coin.no_age_commitment + ? NULL + : &cdi->coin.h_age_commitment, + NULL != bdc->policy_json ? &bdc->h_policy : NULL, + &cdi->coin.denom_pub_hash, + bd->wallet_timestamp, + &bd->merchant_pub, + bd->refund_deadline, + &cdi->coin.coin_pub, + &cdi->csig)) + { + TALER_LOG_WARNING ("Invalid signature on /batch-deposit request\n"); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID, + TALER_B2S (&cdi->coin.coin_pub))) + ? GNUNET_NO + : GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Run processing phase that parses the request. + * + * @param[in,out] bdc request context + * @param root JSON object that was POSTed + */ +static void +bdc_phase_parse (struct BatchDepositContext *bdc, + const json_t *root) +{ + struct TALER_EXCHANGEDB_BatchDeposit *bd = &bdc->bd; + const json_t *coins; + const json_t *policy_json; + bool no_refund_deadline = true; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_full_payto_uri ("merchant_payto_uri", + &bd->receiver_wire_account), + GNUNET_JSON_spec_fixed_auto ("wire_salt", + &bd->wire_salt), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &bd->merchant_pub), + GNUNET_JSON_spec_fixed_auto ("merchant_sig", + &bd->merchant_sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &bd->h_contract_terms), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("wallet_data_hash", + &bd->wallet_data_hash), + &bd->no_wallet_data_hash), + GNUNET_JSON_spec_array_const ("coins", + &coins), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("policy", + &policy_json), + &bdc->has_no_policy), + GNUNET_JSON_spec_timestamp ("timestamp", + &bd->wallet_timestamp), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("extra_wire_subject_metadata", + &bd->extra_wire_subject_metadata), + &bd->no_wallet_data_hash), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("refund_deadline", + &bd->refund_deadline), + &no_refund_deadline), + GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", + &bd->wire_deadline), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (bdc->rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + /* hard failure */ + GNUNET_break (0); + finish_loop (bdc, + MHD_NO); + return; + } + if (GNUNET_NO == res) + { + /* failure */ + GNUNET_break_op (0); + finish_loop (bdc, + MHD_YES); + return; + } + } + if (! TALER_is_valid_subject_metadata_string ( + bd->extra_wire_subject_metadata)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "extra_wire_subject_metadata")); + return; + } + if (GNUNET_OK != + TALER_merchant_contract_verify ( + &bd->h_contract_terms, + &bd->merchant_pub, + &bd->merchant_sig)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "merchant_sig")); + return; + } + bdc->policy_json + = json_incref ((json_t *) policy_json); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Batch deposit into contract %s\n", + GNUNET_h2s (&bd->h_contract_terms.hash)); + + /* validate merchant's wire details (as far as we can) */ + { + char *emsg; + + emsg = TALER_payto_validate (bd->receiver_wire_account); + if (NULL != emsg) + { + MHD_RESULT ret; + + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + ret = TALER_MHD_reply_with_error (bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + emsg); + GNUNET_free (emsg); + finish_loop (bdc, + ret); + return; + } + } + if (GNUNET_TIME_timestamp_cmp (bd->refund_deadline, + >, + bd->wire_deadline)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE, + NULL)); + return; + } + if (GNUNET_TIME_absolute_is_never (bd->wire_deadline.abs_time)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER, + NULL)); + return; + } + TALER_full_payto_hash (bd->receiver_wire_account, + &bd->wire_target_h_payto); + TALER_merchant_wire_signature_hash (bd->receiver_wire_account, + &bd->wire_salt, + &bdc->h_wire); + bd->num_cdis = json_array_size (coins); + if (0 == bd->num_cdis) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "coins")); + return; + } + if (TALER_MAX_COINS < bd->num_cdis) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + bdc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "coins")); + return; + } + + bdc->cdis + = GNUNET_new_array (bd->num_cdis, + struct TALER_EXCHANGEDB_CoinDepositInformation); + bdc->deposit_fees + = GNUNET_new_array (bd->num_cdis, + struct TALER_Amount); + bd->cdis = bdc->cdis; + for (unsigned i = 0; i<bd->num_cdis; i++) + { + struct TALER_Amount amount_without_fee; + enum GNUNET_GenericReturnValue res; + + res = parse_coin (bdc, + json_array_get (coins, + i), + &bdc->cdis[i], + &bdc->deposit_fees[i]); + if (GNUNET_OK != res) + { + finish_loop (bdc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + GNUNET_assert (0 <= + TALER_amount_subtract ( + &amount_without_fee, + &bdc->cdis[i].amount_with_fee, + &bdc->deposit_fees[i])); + + GNUNET_assert (0 <= + TALER_amount_add ( + &bdc->accumulated_total_without_fee, + &bdc->accumulated_total_without_fee, + &amount_without_fee)); + } + + GNUNET_JSON_parse_free (spec); + bdc->phase++; +} + + +/** + * Function called to clean up a context. + * + * @param rc request context with data to clean up + */ +static void +bdc_cleaner (struct TEH_RequestContext *rc) +{ + struct BatchDepositContext *bdc = rc->rh_ctx; + + if (NULL != bdc->lch) + { + TEH_legitimization_check_cancel (bdc->lch); + bdc->lch = NULL; + } + if (0 != bdc->cdis) + { + for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) + TALER_denom_sig_free (&bdc->cdis[i].coin.denom_sig); + GNUNET_free (bdc->cdis); + } + GNUNET_free (bdc->deposit_fees); + json_decref (bdc->policy_json); + GNUNET_free (bdc); +} + + +MHD_RESULT +TEH_handler_batch_deposit (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + struct BatchDepositContext *bdc = rc->rh_ctx; + + (void) args; + if (NULL == bdc) + { + bdc = GNUNET_new (struct BatchDepositContext); + bdc->rc = rc; + rc->rh_ctx = bdc; + rc->rh_cleaner = &bdc_cleaner; + bdc->phase = BDC_PHASE_PARSE; + bdc->exchange_timestamp = GNUNET_TIME_timestamp_get (); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &bdc->accumulated_total_without_fee)); + } + while (1) + { + switch (bdc->phase) + { + case BDC_PHASE_INIT: + GNUNET_break (0); + bdc->phase = BDC_PHASE_RETURN_NO; + break; + case BDC_PHASE_PARSE: + bdc_phase_parse (bdc, + root); + break; + case BDC_PHASE_POLICY: + bdc_phase_policy (bdc); + break; + case BDC_PHASE_KYC: + bdc_phase_kyc (bdc); + break; + case BDC_PHASE_TRANSACT: + bdc_phase_transact (bdc); + break; + case BDC_PHASE_REPLY_SUCCESS: + bdc_phase_reply_success (bdc); + break; + case BDC_PHASE_SUSPENDED: + return MHD_YES; + case BDC_PHASE_CHECK_KYC_RESULT: + bdc_phase_check_kyc_result (bdc); + break; + case BDC_PHASE_GENERATE_REPLY_FAILURE: + return MHD_queue_response (bdc->rc->connection, + bdc->http_status, + bdc->response); + case BDC_PHASE_RETURN_YES: + return MHD_YES; + case BDC_PHASE_RETURN_NO: + return MHD_NO; + } + } +} + + +/* end of taler-exchange-httpd_batch-deposit.c */ diff --git a/src/exchange/taler-exchange-httpd_post-batch-deposit.h b/src/exchange/taler-exchange-httpd_post-batch-deposit.h @@ -0,0 +1,57 @@ +/* + This file is part of TALER + Copyright (C) 2014 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-batch-deposit.h + * @brief Handle /batch-deposit requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_BATCH_DEPOSIT_H +#define TALER_EXCHANGE_HTTPD_POST_BATCH_DEPOSIT_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Resumes all suspended batch deposit requests + * during cleanup. + */ +void +TEH_batch_deposit_cleanup (void); + + +/** + * Handle a "/batch-deposit" request. Parses the JSON, and, if + * successful, passes the JSON data to #deposit_transaction() to + * further check the details of the operation specified. If everything checks + * out, this will ultimately lead to the "/batch-deposit" being executed, or + * rejected. + * + * @param rc request context + * @param root uploaded JSON data + * @param args arguments, empty in this case + * @return MHD result code + */ +MHD_RESULT +TEH_handler_batch_deposit (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-blinding-prepare.c b/src/exchange/taler-exchange-httpd_post-blinding-prepare.c @@ -0,0 +1,223 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-blinding-prepare.c + * @brief Handle /blinding-prepare requests + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_post-blinding-prepare.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + +MHD_RESULT +TEH_handler_blinding_prepare (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + struct TALER_BlindingMasterSeedP blinding_seed; + const char *cipher; + const char *operation; + const json_t *j_nks; + size_t num; + bool is_melt; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("cipher", + &cipher), + GNUNET_JSON_spec_string ("operation", + &operation), + GNUNET_JSON_spec_fixed_auto ("seed", + &blinding_seed), + GNUNET_JSON_spec_array_const ("nks", + &j_nks), + GNUNET_JSON_spec_end () + }; + + (void) args; + { + 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; + } + if (0 == strcmp (operation, + "melt")) + { + is_melt = true; + } + else if (0 == strcmp (operation, + "withdraw")) + { + is_melt = false; + } + else + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "operation"); + } + + num = json_array_size (j_nks); + if ((0 == num) || + (TALER_MAX_COINS < num)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE, + "nks"); + } + if (0 != strcmp (cipher, "CS")) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + " cipher"); + } + + { + uint32_t cs_indices[num]; + struct TALER_DenominationHashP h_denom_pubs[num]; + struct GNUNET_CRYPTO_CsSessionNonce nonces[num]; + + for (size_t i = 0; i < num; i++) + { + enum GNUNET_GenericReturnValue res; + struct GNUNET_JSON_Specification nks_spec[] = { + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &h_denom_pubs[i]), + GNUNET_JSON_spec_uint32 ("coin_offset", + &cs_indices[i]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_array (rc->connection, + j_nks, + nks_spec, + i, + -1); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES; + + if (TALER_MAX_COINS < cs_indices[i]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "nks"); + } + } + + TALER_cs_derive_nonces_from_seed (&blinding_seed, + is_melt, + num, + cs_indices, + nonces); + { + struct TEH_KeyStateHandle *ksh; + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[num]; + size_t err_idx; + enum TALER_ErrorCode ec; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + + ec = + TEH_keys_denomination_cs_batch_r_pub (ksh, + num, + h_denom_pubs, + nonces, + is_melt, + r_pubs, + &err_idx); + switch (ec) + { + case TALER_EC_NONE: + break; + case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN: + return TEH_RESPONSE_reply_unknown_denom_pub_hash ( + rc->connection, + &h_denom_pubs[err_idx]); + break; + case TALER_EC_EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION: + return TEH_RESPONSE_reply_invalid_denom_cipher_for_operation ( + rc->connection, + &h_denom_pubs[err_idx]); + break; + case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED: + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &h_denom_pubs[err_idx], + ec, + "blinding-prepare"); + break; + case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE: + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &h_denom_pubs[err_idx], + ec, + "blinding-prepare"); + break; + default: + GNUNET_break (0); + return TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); + break; + } + + /* Finally, create the response */ + { + struct TALER_BlindingPrepareResponse response = { + .num = num, + .cipher = GNUNET_CRYPTO_BSA_CS, + .details.cs = r_pubs, + }; + + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + TALER_JSON_pack_blinding_prepare_response (NULL, + &response)); + } + } + } +} + + +/* end of taler-exchange-httpd_blinding_prepare.c */ diff --git a/src/exchange/taler-exchange-httpd_post-blinding-prepare.h b/src/exchange/taler-exchange-httpd_post-blinding-prepare.h @@ -0,0 +1,40 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-blinding-prepare.h + * @brief Handle /blinding-prepare requests + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_BLINDING_PREPARE_H +#define TALER_EXCHANGE_HTTPD_POST_BLINDING_PREPARE_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + +/** + * Handle a "/blinding-prepare" request. + * + * @param rc request context + * @param root uploaded JSON data + * @param args empty array + * @return MHD result code + */ +MHD_RESULT +TEH_handler_blinding_prepare (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-coins-COIN_PUB-refund.c b/src/exchange/taler-exchange-httpd_post-coins-COIN_PUB-refund.c @@ -0,0 +1,369 @@ +/* + This file is part of TALER + Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-coins-COIN_PUB-refund.c + * @brief Handle refund requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_post-coins-COIN_PUB-refund.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * How often do we retry after soft database errors? + */ +#define MAX_RETRIES 3 + + +/** + * Generate successful refund confirmation message. + * + * @param connection connection to the client + * @param coin_pub public key of the coin + * @param refund details about the successful refund + * @return MHD result code + */ +static MHD_RESULT +reply_refund_success (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_EXCHANGEDB_RefundListEntry *refund) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_refund_confirmation_sign ( + &TEH_keys_exchange_sign_, + &refund->h_contract_terms, + coin_pub, + &refund->merchant_pub, + refund->rtransaction_id, + &refund->refund_amount, + &pub, + &sig))) + { + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * Closure for refund_transaction(). + */ +struct RefundContext +{ + /** + * Details about the deposit operation. + */ + const struct TALER_EXCHANGEDB_Refund *refund; + + /** + * Deposit fee of the coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Unique ID of the coin in known_coins. + */ + uint64_t known_coin_id; +}; + + +/** + * Execute a "/refund" transaction. Returns a confirmation that the + * refund was successful, or a failure if we are not aware of a + * matching /deposit or if it is too late to do the refund. + * + * 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 closure with a `const struct TALER_EXCHANGEDB_Refund *` + * @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 +refund_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct RefundContext *rctx = cls; + const struct TALER_EXCHANGEDB_Refund *refund = rctx->refund; + enum GNUNET_DB_QueryStatus qs; + bool not_found; + bool refund_ok; + bool conflict; + bool gone; + + /* Finally, store new refund data */ + qs = TEH_plugin->do_refund (TEH_plugin->cls, + refund, + &rctx->deposit_fee, + rctx->known_coin_id, + &not_found, + &refund_ok, + &gone, + &conflict); + 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 refund"); + return qs; + } + + if (gone) + { + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_GONE, + TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (conflict) + { + GNUNET_break_op (0); + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, + &refund->coin.denom_pub_hash, + &refund->coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (not_found) + { + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_REFUND_DEPOSIT_NOT_FOUND, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! refund_ok) + { + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT, + &refund->coin.denom_pub_hash, + &refund->coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} + + +/** + * We have parsed the JSON information about the refund, do some basic + * sanity checks (especially that the signature on the coin is valid) + * and then execute the refund. Note that we need the DB to check + * the fee structure, so this is not done here. + * + * @param connection the MHD connection to handle + * @param[in,out] refund information about the refund + * @return MHD result code + */ +static MHD_RESULT +verify_and_execute_refund (struct MHD_Connection *connection, + struct TALER_EXCHANGEDB_Refund *refund) +{ + struct RefundContext rctx = { + .refund = refund + }; + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_merchant_refund_verify (&refund->coin.coin_pub, + &refund->details.h_contract_terms, + refund->details.rtransaction_id, + &refund->details.refund_amount, + &refund->details.merchant_pub, + &refund->details.merchant_sig)) + { + TALER_LOG_WARNING ("Invalid signature on refund request\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID, + NULL); + } + + /* Fetch the coin's denomination (hash) */ + for (unsigned int i = 0; i < MAX_RETRIES; i++) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls, + &refund->coin.coin_pub, + &rctx.known_coin_id, + &refund->coin.denom_pub_hash); + switch (qs) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + if (i < MAX_RETRIES - 1) + continue; + /* otherwise: fall-through */ + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_coin_denomination"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + MHD_RESULT res; + char *dhs; + + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + dhs = GNUNET_STRINGS_data_to_string_alloc ( + &refund->coin.denom_pub_hash, + sizeof (refund->coin.denom_pub_hash)); + res = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_REFUND_COIN_NOT_FOUND, + dhs); + GNUNET_free (dhs); + return res; + } + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + { + /* Obtain information about the coin's denomination! */ + struct TEH_DenominationKey *dk; + MHD_RESULT mret; + + dk = TEH_keys_denomination_by_hash (&refund->coin.denom_pub_hash, + connection, + &mret); + if (NULL == dk) + { + /* DKI not found, but we do have a coin with this DK in our database; + not good... */ + GNUNET_break (0); + return mret; + } + refund->details.refund_fee = dk->meta.fees.refund; + rctx.deposit_fee = dk->meta.fees.deposit; + } + + /* Finally run the actual transaction logic */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "run refund", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &refund_transaction, + &rctx)) + { + return mhd_ret; + } + } + return reply_refund_success (connection, + &refund->coin.coin_pub, + &refund->details); +} + + +/** + * Handle a "/coins/$COIN_PUB/refund" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_refund() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_refund (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root) +{ + struct TALER_EXCHANGEDB_Refund refund = { + .details.refund_fee.currency = {0} /* set to invalid, just to be sure */ + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("refund_amount", + TEH_currency, + &refund.details.refund_amount), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &refund.details.h_contract_terms), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &refund.details.merchant_pub), + GNUNET_JSON_spec_uint64 ("rtransaction_id", + &refund.details.rtransaction_id), + GNUNET_JSON_spec_fixed_auto ("merchant_sig", + &refund.details.merchant_sig), + GNUNET_JSON_spec_end () + }; + + refund.coin.coin_pub = *coin_pub; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + { + MHD_RESULT res; + + res = verify_and_execute_refund (connection, + &refund); + GNUNET_JSON_parse_free (spec); + return res; + } +} + + +/* end of taler-exchange-httpd_refund.c */ diff --git a/src/exchange/taler-exchange-httpd_post-coins-COIN_PUB-refund.h b/src/exchange/taler-exchange-httpd_post-coins-COIN_PUB-refund.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-coins-COIN_PUB-refund.h + * @brief Handle /refund requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_COINS_COIN_PUB_REFUND_H +#define TALER_EXCHANGE_HTTPD_POST_COINS_COIN_PUB_REFUND_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/coins/$COIN_PUB/refund" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_refund() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_refund (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-kyc-start-ID.c b/src/exchange/taler-exchange-httpd_post-kyc-start-ID.c @@ -0,0 +1,537 @@ +/* + This file is part of TALER + Copyright (C) 2021-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-kyc-start-ID.c + * @brief Handle request for starting a KYC process with an external provider. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_post-kyc-start-ID.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * POST request in asynchronous processing. + */ +struct KycPoller +{ + + /** + * Access token for the KYC data of the account. + */ + struct TALER_AccountAccessTokenP access_token; + + /** + * Authorization hash for the selected measure. + */ + struct TALER_KycMeasureAuthorizationHashP shv; + + /** + * Hash of the payto:// URI we are starting to the KYC for. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * Kept in a DLL. + */ + struct KycPoller *next; + + /** + * Kept in a DLL. + */ + struct KycPoller *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Logic for @e ih + */ + struct TALER_KYCLOGIC_Plugin *ih_logic; + + /** + * Handle to asynchronously running KYC initiation + * request. + */ + struct TALER_KYCLOGIC_InitiateHandle *ih; + + /** + * Set of applicable KYC measures. + */ + json_t *jmeasures; + + /** + * Where to redirect the user to start the KYC process. + */ + char *redirect_url; + + /** + * Set to the name of the KYC provider. + */ + const char *provider_name; + + /** + * Set to error details, on error (@ec not TALER_EC_NONE). + */ + char *hint; + + /** + * Row of the requirement being started. + */ + unsigned long long legitimization_measure_serial_id; + + /** + * Row of KYC process being initiated. + */ + uint64_t process_row; + + /** + * Index of the measure this upload is for. + */ + unsigned int measure_index; + + /** + * Set to error encountered with KYC logic, if any. + */ + enum TALER_ErrorCode ec; + + /** + * True if we are still suspended. + */ + bool suspended; + + /** + * True if @e h_payto is for a wallet + */ + bool is_wallet; + +}; + + +/** + * Head of list of requests in asynchronous processing. + */ +static struct KycPoller *kyp_head; + +/** + * Tail of list of requests in asynchronous processing. + */ +static struct KycPoller *kyp_tail; + + +void +TEH_kyc_start_cleanup () +{ + struct KycPoller *kyp; + + while (NULL != (kyp = kyp_head)) + { + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + if (NULL != kyp->ih) + { + kyp->ih_logic->initiate_cancel (kyp->ih); + kyp->ih = NULL; + } + if (kyp->suspended) + { + kyp->suspended = false; + MHD_resume_connection (kyp->connection); + } + } +} + + +/** + * Function called once a connection is done to + * clean up the `struct ReservePoller` state. + * + * @param rc context to clean up for + */ +static void +kyp_cleanup (struct TEH_RequestContext *rc) +{ + struct KycPoller *kyp = rc->rh_ctx; + + GNUNET_assert (! kyp->suspended); + if (NULL != kyp->ih) + { + kyp->ih_logic->initiate_cancel (kyp->ih); + kyp->ih = NULL; + } + GNUNET_free (kyp->redirect_url); + GNUNET_free (kyp->hint); + json_decref (kyp->jmeasures); + GNUNET_free (kyp); +} + + +/** + * Function called with the result of a KYC initiation + * operation. + * + * @param cls closure with our `struct KycPoller *` + * @param ec #TALER_EC_NONE on success + * @param redirect_url set to where to redirect the user on success, NULL on failure + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param error_msg_hint set to additional details to return to user, NULL on success + */ +static void +initiate_cb ( + void *cls, + enum TALER_ErrorCode ec, + const char *redirect_url, + const char *provider_user_id, + const char *provider_legitimization_id, + const char *error_msg_hint) +{ + struct KycPoller *kyp = cls; + enum GNUNET_DB_QueryStatus qs; + + kyp->ih = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC initiation `%s' completed with ec=%d (%s)\n", + provider_legitimization_id, + ec, + (TALER_EC_NONE == ec) + ? redirect_url + : error_msg_hint); + kyp->ec = ec; + if (TALER_EC_NONE == ec) + { + kyp->redirect_url = GNUNET_strdup (redirect_url); + } + else + { + kyp->hint = GNUNET_strdup (error_msg_hint); + } + qs = TEH_plugin->update_kyc_process_by_row ( + TEH_plugin->cls, + kyp->process_row, + kyp->provider_name, + &kyp->h_payto, + provider_user_id, + provider_legitimization_id, + redirect_url, + GNUNET_TIME_UNIT_ZERO_ABS, + ec, + error_msg_hint, + TALER_EC_NONE != ec); + if (qs <= 0) + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC requirement update failed for %s with status %d at %s:%u\n", + TALER_B2S (&kyp->h_payto), + qs, + __FILE__, + __LINE__); + GNUNET_assert (kyp->suspended); + kyp->suspended = false; + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + MHD_resume_connection (kyp->connection); + TALER_MHD_daemon_trigger (); +} + + +MHD_RESULT +TEH_handler_kyc_start ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]) +{ + struct KycPoller *kyp = rc->rh_ctx; + + (void) root; + if (NULL == kyp) + { + const char *id = args[0]; + enum GNUNET_DB_QueryStatus qs; + const struct TALER_KYCLOGIC_KycProvider *provider; + struct TALER_KYCLOGIC_ProviderDetails *pd; + bool is_finished; + const json_t *context; + + kyp = GNUNET_new (struct KycPoller); + kyp->connection = rc->connection; + rc->rh_ctx = kyp; + rc->rh_cleaner = &kyp_cleanup; + + { + char dummy; + const char *slash; + + slash = strchr (id, '-'); + if (NULL == slash) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (id, + slash - id, + &kyp->shv, + sizeof (kyp->shv))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "Authorization hash in ID is malformed"); + } + if (2 != + sscanf (slash + 1, + "%u-%llu%c", + &kyp->measure_index, + &kyp->legitimization_measure_serial_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "ID is malformed"); + } + } + qs = TEH_plugin->lookup_pending_legitimization ( + TEH_plugin->cls, + kyp->legitimization_measure_serial_id, + &kyp->access_token, + &kyp->h_payto, + &kyp->jmeasures, + &is_finished, + &kyp->is_wallet); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_pending_legitimization"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + if (is_finished) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED, + rc->url); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found pending legitimization %llu with measures\n", + (unsigned long long) kyp->legitimization_measure_serial_id); + json_dumpf (kyp->jmeasures, + stderr, + JSON_INDENT (2)); + fprintf (stderr, + "\n"); + + { + struct TALER_KycMeasureAuthorizationHashP shv2; + + TALER_kyc_measure_authorization_hash ( + &kyp->access_token, + kyp->legitimization_measure_serial_id, + kyp->measure_index, + &shv2); + if (0 != + GNUNET_memcmp (&kyp->shv, + &shv2)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + } + + { + const char *check_name; + const char *prog_name; + + kyp->ec = TALER_KYCLOGIC_select_measure ( + kyp->jmeasures, + kyp->measure_index, + &check_name, + &prog_name, + &context); + if (TALER_EC_NONE != kyp->ec) + { + /* return EC in next call to this function */ + GNUNET_break_op (0); + kyp->hint + = GNUNET_strdup ("TALER_KYCLOGIC_select_measure"); + return MHD_YES; + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Selected measure is %llu with check `%s'\n", + (unsigned long long) kyp->measure_index, + check_name); + if (NULL != context) + { + json_dumpf (context, + stderr, + JSON_INDENT (2)); + fprintf (stderr, + "\n"); + } + + provider = TALER_KYCLOGIC_check_to_provider ( + check_name); + if (NULL == provider) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_KYC_INVALID_LOGIC_TO_CHECK, + check_name); + } + } + + TALER_KYCLOGIC_provider_to_logic ( + provider, + &kyp->ih_logic, + &pd, + &kyp->provider_name); + + /* FIXME-#9419: the next two DB interactions should be ONE + transaction */ + /* Check if we already initiated this process */ + qs = TEH_plugin->get_pending_kyc_requirement_process ( + TEH_plugin->cls, + &kyp->h_payto, + kyp->provider_name, + &kyp->redirect_url); + if (qs < 0) + { + GNUNET_break (0); + /* Simple query, never should be a soft error. */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_pending_kyc_requirement_process"); + } + if (NULL != kyp->redirect_url) + return MHD_YES; /* success, return the redirect URL + (in next call to this function) */ + + /* set up new requirement process */ + qs = TEH_plugin->insert_kyc_requirement_process ( + TEH_plugin->cls, + &kyp->h_payto, + kyp->measure_index, + kyp->legitimization_measure_serial_id, + kyp->provider_name, + NULL, /* provider_account_id */ + NULL, /* provider_legitimziation_id */ + &kyp->process_row); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_process"); + } + + kyp->ih = kyp->ih_logic->initiate ( + kyp->ih_logic->cls, + pd, + &kyp->h_payto, + kyp->process_row, + context, + &initiate_cb, + kyp); + if (NULL == kyp->ih) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG, + "initiate KYC process"); + } + kyp->suspended = true; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (kyp->connection); + return MHD_YES; + } + + if ( (TALER_EC_NONE != kyp->ec) || + (NULL == kyp->redirect_url) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC process failed to start with error %d (%s)\n", + (int) kyp->ec, + kyp->hint); + if (TALER_EC_NONE == kyp->ec) + { + GNUNET_break (0); + kyp->ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + return TALER_MHD_reply_with_ec (rc->connection, + kyp->ec, + kyp->hint); + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("redirect_url", + kyp->redirect_url)); +} + + +/* end of taler-exchange-httpd_kyc-start.c */ diff --git a/src/exchange/taler-exchange-httpd_post-kyc-start-ID.h b/src/exchange/taler-exchange-httpd_post-kyc-start-ID.h @@ -0,0 +1,50 @@ +/* + This file is part of TALER + Copyright (C) 2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-kyc-start-ID.h + * @brief Handle /kyc-start requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_KYC_START_ID_H +#define TALER_EXCHANGE_HTTPD_POST_KYC_START_ID_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Resume suspended requests for /kyc-start. + */ +void +TEH_kyc_start_cleanup (void); + + +/** + * Handle a "/kyc-start" request. + * + * @param rc request to handle + * @param root uploaded JSON data (empty by current API) + * @param args array with the ID + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_start ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-kyc-upload-ID.c b/src/exchange/taler-exchange-httpd_post-kyc-upload-ID.c @@ -0,0 +1,567 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-kyc-upload-ID.c + * @brief Handle /kyc-upload/$ID request + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_post-kyc-upload-ID.h" + +#define MAX_RETRIES 3 + +/** + * Context used for processing the KYC upload req + */ +struct UploadContext +{ + + /** + * Kept in a DLL. + */ + struct UploadContext *next; + + /** + * Kept in a DLL. + */ + struct UploadContext *prev; + + /** + * Access token for the KYC data of the account. + */ + struct TALER_AccountAccessTokenP access_token; + + /** + * Index of the measure this upload is for. + */ + unsigned int measure_index; + + /** + * HTTP status code to use with @e response. + */ + unsigned int response_code; + + /** + * Index in the legitimization measures table this ID + * refers to. + */ + unsigned long long legitimization_measure_serial_id; + + /** + * Response to return, NULL if none yet. + */ + struct MHD_Response *response; + + /** + * Request we are processing. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for async KYC processing. + */ + struct TEH_KycMeasureRunContext *kat; + + /** + * Uploaded data, in JSON. + */ + const json_t *result; + + /** + * Set by the transaction to the legitimization process row. + */ + uint64_t legi_process_row; + + /** + * Set by the transaction to the affected account payto hash. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * Set by the transaction to true if the account is for a wallet. + */ + bool is_wallet; + +}; + + +/** + * Kept in a DLL. + */ +static struct UploadContext *uc_head; + +/** + * Kept in a DLL. + */ +static struct UploadContext *uc_tail; + + +void +TEH_kyc_upload_cleanup () +{ + struct UploadContext *uc; + + while (NULL != (uc = uc_head)) + { + MHD_resume_connection (uc->rc->connection); + GNUNET_CONTAINER_DLL_remove (uc_head, + uc_tail, + uc); + } +} + + +/** + * Function called to clean up upload context. + * + * @param[in,out] rc context to clean up + */ +static void +upload_cleaner (struct TEH_RequestContext *rc) +{ + struct UploadContext *uc = rc->rh_ctx; + + if (NULL != uc->kat) + { + TEH_kyc_run_measure_cancel (uc->kat); + uc->kat = NULL; + } + if (NULL != uc->response) + { + MHD_destroy_response (uc->response); + uc->response = NULL; + } + GNUNET_free (uc); +} + + +/** + * Function called after the KYC-AML trigger is done. + * + * @param cls closure + * @param ec error code or 0 on success + * @param detail error message or NULL on success / no info + */ +static void +aml_trigger_callback ( + void *cls, + enum TALER_ErrorCode ec, + const char *detail) +{ + struct UploadContext *uc = cls; + + uc->kat = NULL; + GNUNET_assert (NULL == uc->response); + if (TALER_EC_NONE != ec) + { + uc->response_code = TALER_ErrorCode_get_http_status (ec); + if (0 == uc->response_code) + { + GNUNET_break (0); + uc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + } + GNUNET_assert (uc->response_code != UINT_MAX); + uc->response = TALER_MHD_make_error ( + ec, + detail); + } + else + { + uc->response_code = MHD_HTTP_NO_CONTENT; + uc->response = MHD_create_response_from_buffer_static ( + 0, + "" + ); + TALER_MHD_add_global_headers (uc->response, + true); + } + + MHD_resume_connection (uc->rc->connection); + GNUNET_CONTAINER_DLL_remove (uc_head, + uc_tail, + uc); + TALER_MHD_daemon_trigger (); +} + + +/** + * Do the main database transaction. + * + * @param cls closure with a `struct UploadContext` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + */ +static enum GNUNET_DB_QueryStatus +transact (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct UploadContext *uc = cls; + struct TEH_RequestContext *rc = uc->rc; + enum GNUNET_DB_QueryStatus qs; + json_t *jmeasures; + bool is_finished = false; + size_t enc_attributes_len; + void *enc_attributes; + const char *error_message; + char *form_name; + enum TALER_ErrorCode ec; + + qs = TEH_plugin->lookup_completed_legitimization ( + TEH_plugin->cls, + uc->legitimization_measure_serial_id, + uc->measure_index, + &uc->access_token, + &uc->h_payto, + &uc->is_wallet, + &jmeasures, + &is_finished, + &enc_attributes_len, + &enc_attributes); + /* FIXME: not exactly performant/elegant, should eventually + modify lookup_completed_legitimization to + return something if we are purely pending? */ + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_completed_legitimization"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + qs = TEH_plugin->lookup_pending_legitimization ( + TEH_plugin->cls, + uc->legitimization_measure_serial_id, + &uc->access_token, + &uc->h_payto, + &jmeasures, + &is_finished, + &uc->is_wallet); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_pending_legitimization"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if (NULL != enc_attributes) + { + json_t *xattributes; + + xattributes + = TALER_CRYPTO_kyc_attributes_decrypt ( + &TEH_attribute_key, + enc_attributes, + enc_attributes_len); + if (json_equal (xattributes, + uc->result)) + { + /* Request is idempotent! */ + json_decref (xattributes); + GNUNET_free (enc_attributes); + json_decref (jmeasures); + if (is_finished) + { + *mhd_ret = TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + /* Note: problem below is not here, but likely some previous + upload of the attributes failed badly in an AML program. */ + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG, + "attributes known, but legitimization process failed"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + json_decref (xattributes); + GNUNET_free (enc_attributes); + json_decref (jmeasures); + /* Form was already done with with different attributes, conflict! */ + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (is_finished) + { + /* This should not be possible (is_finished but NULL==enc_attributes), + but also we should not run logic again if we are finished. */ + GNUNET_break_op (0); + json_decref (jmeasures); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + ec = TALER_KYCLOGIC_check_form (jmeasures, + uc->measure_index, + uc->result, + &form_name, + &error_message); + if (TALER_EC_NONE != ec) + { + GNUNET_break_op (0); + json_decref (jmeasures); + *mhd_ret = TALER_MHD_reply_with_ec ( + rc->connection, + ec, + error_message); + return GNUNET_DB_STATUS_HARD_ERROR; + } + json_decref (jmeasures); + + /* Setup KYC process (which we will then immediately 'finish') */ + qs = TEH_plugin->insert_kyc_requirement_process ( + TEH_plugin->cls, + &uc->h_payto, + uc->measure_index, + uc->legitimization_measure_serial_id, + form_name, + NULL, /* provider account ID */ + NULL, /* provider legi ID */ + &uc->legi_process_row); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + GNUNET_free (form_name); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_process"); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_free (form_name); + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + GNUNET_free (form_name); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "insert_kyc_requirement_process"); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + qs = TEH_kyc_store_attributes ( + uc->legi_process_row, + &uc->h_payto, + form_name, + NULL /* provider account */, + NULL /* provider legi ID */, + GNUNET_TIME_UNIT_FOREVER_ABS, /* expiration time */ + uc->result); + GNUNET_free (form_name); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "kyc_store_attributes"); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "kyc_store_attributes"); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return qs; + } + GNUNET_assert (0); + *mhd_ret = MHD_NO; + return GNUNET_DB_STATUS_HARD_ERROR; +} + + +MHD_RESULT +TEH_handler_kyc_upload ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]) +{ + struct UploadContext *uc = rc->rh_ctx; + const char *id = args[0]; + + if (NULL == uc) + { + const char *slash; + char dummy; + + uc = GNUNET_new (struct UploadContext); + uc->rc = rc; + uc->result = root; + rc->rh_ctx = uc; + rc->rh_cleaner = &upload_cleaner; + slash = strchr (id, '-'); + if (NULL == slash) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + rc->url); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (id, + slash - id, + &uc->access_token, + sizeof (uc->access_token))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "Access token in ID is malformed"); + } + if (2 != + sscanf (slash + 1, + "%u-%llu%c", + &uc->measure_index, + &uc->legitimization_measure_serial_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "ID is malformed"); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "/kyc-upload received form submission\n"); + if ( (NULL != root) && + (! json_is_string (json_object_get (root, + "FORM_ID"))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "FORM_ID"); + } + json_dumpf (root, + stderr, + JSON_INDENT (2)); + } + if (NULL != uc->response) + { + return MHD_queue_response (rc->connection, + uc->response_code, + uc->response); + + } + + if (GNUNET_OK != + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SETUP_FAILED, + NULL); + } + + { + MHD_RESULT mhd_ret = -1; + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "kyc-upload", + TEH_MT_REQUEST_KYC_UPLOAD, + &mhd_ret, + &transact, + uc)) + return mhd_ret; + } + + uc->kat = TEH_kyc_run_measure_for_attributes ( + &rc->async_scope_id, + uc->legi_process_row, + &uc->h_payto, + uc->is_wallet, + &aml_trigger_callback, + uc); + if (NULL == uc->kat) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG, + "TEH_kyc_finished"); + } + MHD_suspend_connection (uc->rc->connection); + GNUNET_CONTAINER_DLL_insert (uc_head, + uc_tail, + uc); + return MHD_YES; +} diff --git a/src/exchange/taler-exchange-httpd_post-kyc-upload-ID.h b/src/exchange/taler-exchange-httpd_post-kyc-upload-ID.h @@ -0,0 +1,53 @@ +/* + This file is part of TALER + Copyright (C) 2019, 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-kyc-upload-ID.h + * @brief Handle /kyc-upload/$ID requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_KYC_UPLOAD_ID_H +#define TALER_EXCHANGE_HTTPD_POST_KYC_UPLOAD_ID_H + +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Resume suspended connections, called on shutdown. + */ +void +TEH_kyc_upload_cleanup (void); + + +/** + * Handle a "/kyc-upload/$ID" request. + * + * @param rc request context + * @param root json body being uploaded + * @param args includes the ID from the URL (without "/") + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_upload (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-kyc-wallet.c b/src/exchange/taler-exchange-httpd_post-kyc-wallet.c @@ -0,0 +1,336 @@ +/* + This file is part of TALER + Copyright (C) 2021, 2022, 2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-kyc-wallet.c + * @brief Handle request for wallet for KYC check. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_post-kyc-wallet.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Context for the request. + */ +struct KycRequestContext +{ + + /** + * Kept in a DLL. + */ + struct KycRequestContext *next; + + /** + * Kept in a DLL. + */ + struct KycRequestContext *prev; + + /** + * Handle for legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Payto URI of the reserve. + */ + struct TALER_NormalizedPayto payto_uri; + + /** + * Request context. + */ + struct TEH_RequestContext *rc; + + /** + * Response to return. Note that the response must + * be queued or destroyed by the callee. NULL + * if the legitimization check was successful and the handler should return + * a handler-specific result. + */ + struct MHD_Response *response; + + /** + * Public key of the reserve/wallet this is about. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * The wallet's public key + */ + union TALER_AccountPublicKeyP wallet_pub; + + /** + * Balance threshold crossed by the wallet. + */ + struct TALER_Amount balance; + + /** + * KYC status, with row with the legitimization requirement. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Smallest amount (over any timeframe) that may + * require additional KYC checks (if @a kyc.ok). + */ + struct TALER_Amount next_threshold; + + /** + * When do the current KYC rules possibly expire. + * Only valid if @a kyc.ok. + */ + struct GNUNET_TIME_Timestamp expiration_date; + + /** + * HTTP status code for @a response, or 0 + */ + unsigned int http_status; + +}; + + +/** + * Kept in a DLL. + */ +static struct KycRequestContext *krc_head; + +/** + * Kept in a DLL. + */ +static struct KycRequestContext *krc_tail; + + +void +TEH_kyc_wallet_cleanup () +{ + struct KycRequestContext *krc; + + while (NULL != (krc = krc_head)) + { + GNUNET_CONTAINER_DLL_remove (krc_head, + krc_tail, + krc); + MHD_resume_connection (krc->rc->connection); + } +} + + +/** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Returns the wallet balance. + * + * @param cls closure, a `struct KycRequestContext` + * @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 enum GNUNET_DB_QueryStatus +balance_iterator (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct KycRequestContext *krc = cls; + enum GNUNET_GenericReturnValue ret; + + (void) limit; + ret = cb (cb_cls, + &krc->balance, + GNUNET_TIME_absolute_get ()); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls must be a `struct KycRequestContext *` + * @param lcr legitimization check result + */ +static void +legi_result_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct KycRequestContext *krc = cls; + + TEH_plugin->preflight (TEH_plugin->cls); + krc->lch = NULL; + krc->http_status = lcr->http_status; + krc->response = lcr->response; + krc->kyc = lcr->kyc; + krc->next_threshold = lcr->next_threshold; + krc->expiration_date = lcr->expiration_date; + GNUNET_CONTAINER_DLL_remove (krc_head, + krc_tail, + krc); + MHD_resume_connection (krc->rc->connection); + TALER_MHD_daemon_trigger (); +} + + +/** + * Function to clean up our rh_ctx in @a rc + * + * @param[in,out] rc context to clean up + */ +static void +krc_cleaner (struct TEH_RequestContext *rc) +{ + struct KycRequestContext *krc = rc->rh_ctx; + + if (NULL != krc->lch) + { + TEH_legitimization_check_cancel (krc->lch); + krc->lch = NULL; + } + GNUNET_free (krc->payto_uri.normalized_payto); + GNUNET_free (krc); +} + + +MHD_RESULT +TEH_handler_kyc_wallet ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + struct KycRequestContext *krc = rc->rh_ctx; + + if (NULL == krc) + { + krc = GNUNET_new (struct KycRequestContext); + krc->rc = rc; + rc->rh_ctx = krc; + rc->rh_cleaner = &krc_cleaner; + { + struct TALER_ReserveSignatureP reserve_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &reserve_sig), + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &krc->wallet_pub.reserve_pub), + TALER_JSON_spec_amount ("balance", + TEH_currency, + &krc->balance), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + (void) args; + ret = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_account_setup_verify ( + &krc->wallet_pub.reserve_pub, + &krc->balance, + &reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_KYC_WALLET_SIGNATURE_INVALID, + NULL); + } + } + krc->payto_uri + = TALER_reserve_make_payto (TEH_base_url, + &krc->wallet_pub.reserve_pub); + TALER_normalized_payto_hash (krc->payto_uri, + &krc->h_payto); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "h_payto of wallet %s is %s\n", + krc->payto_uri.normalized_payto, + TALER_B2S (&krc->h_payto)); + { + struct TALER_FullPayto fake_full_payto; + + GNUNET_asprintf (&fake_full_payto.full_payto, + "%s?receiver-name=wallet", + krc->payto_uri.normalized_payto); + krc->lch = TEH_legitimization_check ( + &rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, + fake_full_payto, + &krc->h_payto, + &krc->wallet_pub, + &balance_iterator, + krc, + &legi_result_cb, + krc); + GNUNET_free (fake_full_payto.full_payto); + } + GNUNET_assert (NULL != krc->lch); + MHD_suspend_connection (rc->connection); + GNUNET_CONTAINER_DLL_insert (krc_head, + krc_tail, + krc); + return MHD_YES; + } + if (NULL != krc->response) + return MHD_queue_response (rc->connection, + krc->http_status, + krc->response); + if (krc->kyc.ok) + { + bool have_ts + = TALER_amount_is_valid (&krc->next_threshold); + + /* KYC not required or already satisfied */ + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_timestamp ("expiration_time", + krc->expiration_date), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("next_threshold", + have_ts + ? &krc->next_threshold + : NULL))); + } + return TEH_RESPONSE_reply_kyc_required (rc->connection, + &krc->h_payto, + &krc->kyc, + false); +} + + +/* end of taler-exchange-httpd_kyc-wallet.c */ diff --git a/src/exchange/taler-exchange-httpd_post-kyc-wallet.h b/src/exchange/taler-exchange-httpd_post-kyc-wallet.h @@ -0,0 +1,52 @@ +/* + This file is part of TALER + Copyright (C) 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-kyc-wallet.h + * @brief Handle /kyc-wallet requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_KYC_WALLET_H +#define TALER_EXCHANGE_HTTPD_POST_KYC_WALLET_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_kyc_wallet_cleanup (void); + + +/** + * Handle a "/kyc-wallet" request. Parses the "reserve_pub" EdDSA key of the + * reserve and the signature "reserve_sig" which affirms the operation. If OK, + * a KYC record is created (if missing) and the KYC status returned. + * + * @param rc request to handle + * @param root uploaded JSON data + * @param args empty array + * @return MHD result code + */ +MHD_RESULT +TEH_handler_kyc_wallet ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-management-aml-officers.c b/src/exchange/taler-exchange-httpd_post-management-aml-officers.c @@ -0,0 +1,142 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-aml-officers.c + * @brief Handle request to update AML officer status + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How often do we try the DB operation at most? + */ +#define MAX_RETRIES 10 + + +MHD_RESULT +TEH_handler_management_aml_officers ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct TALER_AmlOfficerPublicKeyP officer_pub; + const char *officer_name; + struct GNUNET_TIME_Timestamp change_date; + bool is_active; + bool read_only; + struct TALER_MasterSignatureP master_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("officer_pub", + &officer_pub), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + GNUNET_JSON_spec_bool ("is_active", + &is_active), + GNUNET_JSON_spec_bool ("read_only", + &read_only), + GNUNET_JSON_spec_string ("officer_name", + &officer_name), + GNUNET_JSON_spec_timestamp ("change_date", + &change_date), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_aml_officer_status_verify ( + &officer_pub, + officer_name, + change_date, + is_active, + read_only, + &TEH_master_public_key, + &master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID, + NULL); + } + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp last_date; + unsigned int retries_left = MAX_RETRIES; + + do { + qs = TEH_plugin->insert_aml_officer (TEH_plugin->cls, + &officer_pub, + &master_sig, + officer_name, + is_active, + read_only, + change_date, + &last_date); + if (0 == --retries_left) + break; + } while (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_aml_officer"); + } + if (GNUNET_TIME_timestamp_cmp (last_date, + >, + change_date)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT, + NULL); + } + } + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_aml-officers.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-auditors-AUDITOR_PUB-disable.c b/src/exchange/taler-exchange-httpd_post-management-auditors-AUDITOR_PUB-disable.c @@ -0,0 +1,196 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-auditors-AUDITOR_PUB-disable.c + * @brief Handle request to disable auditor. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for the #del_auditor transaction. + */ +struct DelAuditorContext +{ + + /** + * Auditor public key this is about. + */ + struct TALER_AuditorPublicKeyP auditor_pub; + + /** + * Auditor URL this is about. + */ + const char *auditor_url; + + /** + * Timestamp for checking against replay attacks. + */ + struct GNUNET_TIME_Timestamp validity_end; + +}; + + +/** + * Function implementing database transaction to del an auditor. 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 closure with a `struct DelAuditorContext` + * @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 +del_auditor (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct DelAuditorContext *dac = cls; + struct GNUNET_TIME_Timestamp last_date; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->lookup_auditor_timestamp (TEH_plugin->cls, + &dac->auditor_pub, + &last_date); + 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, + "lookup auditor"); + return qs; + } + if (GNUNET_TIME_timestamp_cmp (last_date, + >, + dac->validity_end)) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (0 == qs) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + qs = TEH_plugin->update_auditor (TEH_plugin->cls, + &dac->auditor_pub, + "", /* auditor URL */ + "", /* auditor name */ + dac->validity_end, + false); + if (qs < 0) + { + GNUNET_break (0); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "del auditor"); + return qs; + } + TEH_keys_update_states (); + return qs; +} + + +MHD_RESULT +TEH_handler_management_auditors_AP_disable ( + struct MHD_Connection *connection, + const struct TALER_AuditorPublicKeyP *auditor_pub, + const json_t *root) +{ + struct TALER_MasterSignatureP master_sig; + struct DelAuditorContext dac = { + .auditor_pub = *auditor_pub + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + GNUNET_JSON_spec_timestamp ("validity_end", + &dac.validity_end), + GNUNET_JSON_spec_end () + }; + MHD_RESULT res; + enum GNUNET_GenericReturnValue ret; + + ret = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + if (GNUNET_OK != + TALER_exchange_offline_auditor_del_verify ( + auditor_pub, + dac.validity_end, + &TEH_master_public_key, + &master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID, + NULL); + } + + ret = TEH_DB_run_transaction (connection, + "del auditor", + TEH_MT_REQUEST_OTHER, + &res, + &del_auditor, + &dac); + if (GNUNET_SYSERR == ret) + return res; + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_auditors_AP_disable.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-auditors.c b/src/exchange/taler-exchange-httpd_post-management-auditors.c @@ -0,0 +1,207 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2021 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-auditors.c + * @brief Handle request to add auditor. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for the #add_auditor transaction. + */ +struct AddAuditorContext +{ + /** + * Master signature to store. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Auditor public key this is about. + */ + struct TALER_AuditorPublicKeyP auditor_pub; + + /** + * Auditor URL this is about. + */ + const char *auditor_url; + + /** + * Human readable name of the auditor. + */ + const char *auditor_name; + + /** + * Timestamp for checking against replay attacks. + */ + struct GNUNET_TIME_Timestamp validity_start; + +}; + + +/** + * Function implementing database transaction to add an auditor. 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 closure with a `struct AddAuditorContext` + * @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 +add_auditor (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AddAuditorContext *aac = cls; + struct GNUNET_TIME_Timestamp last_date; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->lookup_auditor_timestamp (TEH_plugin->cls, + &aac->auditor_pub, + &last_date); + 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, + "lookup auditor"); + return qs; + } + if ( (0 < qs) && + (GNUNET_TIME_timestamp_cmp (last_date, + >, + aac->validity_start) ) ) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (0 == qs) + qs = TEH_plugin->insert_auditor (TEH_plugin->cls, + &aac->auditor_pub, + aac->auditor_url, + aac->auditor_name, + aac->validity_start); + else + qs = TEH_plugin->update_auditor (TEH_plugin->cls, + &aac->auditor_pub, + aac->auditor_url, + aac->auditor_name, + aac->validity_start, + true); + if (qs < 0) + { + GNUNET_break (0); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "add auditor"); + return qs; + } + TEH_keys_update_states (); + return qs; +} + + +MHD_RESULT +TEH_handler_management_auditors ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct AddAuditorContext aac; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &aac.master_sig), + GNUNET_JSON_spec_fixed_auto ("auditor_pub", + &aac.auditor_pub), + TALER_JSON_spec_web_url ("auditor_url", + &aac.auditor_url), + GNUNET_JSON_spec_string ("auditor_name", + &aac.auditor_name), + GNUNET_JSON_spec_timestamp ("validity_start", + &aac.validity_start), + GNUNET_JSON_spec_end () + }; + MHD_RESULT res; + enum GNUNET_GenericReturnValue ret; + + ret = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + if (GNUNET_OK != + TALER_exchange_offline_auditor_add_verify ( + &aac.auditor_pub, + aac.auditor_url, + aac.validity_start, + &TEH_master_public_key, + &aac.master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID, + NULL); + } + + ret = TEH_DB_run_transaction (connection, + "add auditor", + TEH_MT_REQUEST_OTHER, + &res, + &add_auditor, + &aac); + if (GNUNET_SYSERR == ret) + return res; + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_auditors.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-denominations-H_DENOM_PUB-revoke.c b/src/exchange/taler-exchange-httpd_post-management-denominations-H_DENOM_PUB-revoke.c @@ -0,0 +1,94 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-denominations-H_DENOM_PUB-revoke.c + * @brief Handle denomination revocation requests. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +MHD_RESULT +TEH_handler_management_denominations_HDP_revoke ( + struct MHD_Connection *connection, + const struct TALER_DenominationHashP *h_denom_pub, + const json_t *root) +{ + struct TALER_MasterSignatureP master_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_denomination_revoke_verify ( + h_denom_pub, + &TEH_master_public_key, + &master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID, + NULL); + } + qs = TEH_plugin->insert_denomination_revocation (TEH_plugin->cls, + h_denom_pub, + &master_sig); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "denomination revocation"); + } + TEH_keys_denomination_revoke (h_denom_pub); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_denominations_HDP_revoke.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-drain.c b/src/exchange/taler-exchange-httpd_post-management-drain.c @@ -0,0 +1,195 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-drain.c + * @brief Handle request to drain profits + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for the #drain transaction. + */ +struct DrainContext +{ + /** + * Fee's signature affirming the #TALER_SIGNATURE_MASTER_DRAIN_PROFITS operation. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Wire transfer identifier to use. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Account to credit. + */ + struct TALER_FullPayto payto_uri; + + /** + * Configuration section with account to debit. + */ + const char *account_section; + + /** + * Signature time. + */ + struct GNUNET_TIME_Timestamp date; + + /** + * Amount to transfer. + */ + struct TALER_Amount amount; + +}; + + +/** + * Function implementing database transaction to drain profits. 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 closure with a `struct DrainContext` + * @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 +drain (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct DrainContext *dc = cls; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->insert_drain_profit ( + TEH_plugin->cls, + &dc->wtid, + dc->account_section, + dc->payto_uri, + dc->date, + &dc->amount, + &dc->master_sig); + 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 drain profit"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_management_post_drain ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct DrainContext dc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("debit_account_section", + &dc.account_section), + TALER_JSON_spec_full_payto_uri ("credit_payto_uri", + &dc.payto_uri), + GNUNET_JSON_spec_fixed_auto ("wtid", + &dc.wtid), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &dc.master_sig), + GNUNET_JSON_spec_timestamp ("date", + &dc.date), + TALER_JSON_spec_amount ("amount", + TEH_currency, + &dc.amount), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_profit_drain_verify ( + &dc.wtid, + dc.date, + &dc.amount, + dc.account_section, + dc.payto_uri, + &TEH_master_public_key, + &dc.master_sig)) + { + /* signature invalid */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID, + NULL); + } + + { + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + + res = TEH_DB_run_transaction (connection, + "insert drain profit", + TEH_MT_REQUEST_OTHER, + &ret, + &drain, + &dc); + if (GNUNET_SYSERR == res) + return ret; + } + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_drain.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-extensions.c b/src/exchange/taler-exchange-httpd_post-management-extensions.c @@ -0,0 +1,300 @@ +/* + This file is part of TALER + Copyright (C) 2021 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 <http://www.gnu.org/licenses/> + */ +/** + * @file taler-exchange-httpd_post-management-extensions.c + * @brief Handle request to POST /management/extensions + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_extensions.h" +#include "taler/taler_dbevents.h" + +/** + * Extension carries the necessary data for a particular extension. + * + */ +struct Extension +{ + enum TALER_Extension_Type type; + json_t *manifest; +}; + +/** + * Closure for the #set_extensions transaction + */ +struct SetExtensionsContext +{ + uint32_t num_extensions; + struct Extension *extensions; + struct TALER_MasterSignatureP extensions_sig; +}; + +/** + * Function implementing database transaction to set the manifests of + * extensions. It 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 closure with a `struct SetExtensionsContext` + * @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 +set_extensions (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct SetExtensionsContext *sec = cls; + + /* save the manifests of all extensions */ + for (uint32_t i = 0; i<sec->num_extensions; i++) + { + struct Extension *ext = &sec->extensions[i]; + const struct TALER_Extension *taler_ext; + enum GNUNET_DB_QueryStatus qs; + char *manifest; + + taler_ext = TALER_extensions_get_by_type (ext->type); + if (NULL == taler_ext) + { + /* No such extension found */ + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + manifest = json_dumps (ext->manifest, JSON_COMPACT | JSON_SORT_KEYS); + if (NULL == manifest) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_INVALID, + "convert configuration to string"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->set_extension_manifest ( + TEH_plugin->cls, + taler_ext->name, + manifest); + + free (manifest); + + 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, + "save extension configuration"); + } + + /* Success, trigger event */ + { + uint32_t nbo_type = htonl (sec->extensions[i].type); + struct GNUNET_DB_EventHeaderP ev = { + .size = htons (sizeof (ev)), + .type = htons (TALER_DBEVENT_EXCHANGE_EXTENSIONS_UPDATED) + }; + + TEH_plugin->event_notify (TEH_plugin->cls, + &ev, + &nbo_type, + sizeof(nbo_type)); + } + + } + + /* All extensions configured, update the signature */ + TEH_extensions_sig = sec->extensions_sig; + TEH_extensions_signed = true; + + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* only 'success', so >=0, matters here */ +} + + +static enum GNUNET_GenericReturnValue +verify_extensions_from_json ( + const json_t *extensions, + struct SetExtensionsContext *sec) +{ + const char*name; + const struct TALER_Extension *extension; + size_t i = 0; + json_t *manifest; + + GNUNET_assert (NULL != extensions); + GNUNET_assert (json_is_object (extensions)); + + sec->num_extensions = json_object_size (extensions); + sec->extensions = GNUNET_new_array (sec->num_extensions, + struct Extension); + + json_object_foreach ((json_t *) extensions, name, manifest) + { + int critical = 0; + json_t *config; + const char *version = NULL; + + /* load and verify criticality, version, etc. */ + extension = TALER_extensions_get_by_name (name); + if (NULL == extension) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "no such extension: %s\n", name); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_extensions_parse_manifest ( + manifest, &critical, &version, &config)) + return GNUNET_SYSERR; + + if (critical != extension->critical + || 0 != strcmp (version, extension->version) // FIXME-oec: libtool compare + || NULL == config + || GNUNET_OK != extension->load_config (config, NULL)) + return GNUNET_SYSERR; + + sec->extensions[i].type = extension->type; + sec->extensions[i].manifest = json_copy (manifest); + } + + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_management_post_extensions ( + struct MHD_Connection *connection, + const json_t *root) +{ + MHD_RESULT ret; + const json_t *extensions; + struct SetExtensionsContext sec = {0}; + struct GNUNET_JSON_Specification top_spec[] = { + GNUNET_JSON_spec_object_const ("extensions", + &extensions), + GNUNET_JSON_spec_fixed_auto ("extensions_sig", + &sec.extensions_sig), + GNUNET_JSON_spec_end () + }; + + /* Parse the top level json structure */ + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + top_spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + + /* Verify the signature */ + { + struct TALER_ExtensionManifestsHashP h_manifests; + + if (GNUNET_OK != + TALER_JSON_extensions_manifests_hash (extensions, + &h_manifests) || + GNUNET_OK != + TALER_exchange_offline_extension_manifests_hash_verify ( + &h_manifests, + &TEH_master_public_key, + &sec.extensions_sig)) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "invalid signuture"); + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received /management/extensions\n"); + + /* Now parse individual extensions and signatures from those objects. */ + if (GNUNET_OK != + verify_extensions_from_json (extensions, &sec)) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "invalid object"); + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received %u extensions\n", + sec.num_extensions); + + /* now run the transaction to persist the configurations */ + { + enum GNUNET_GenericReturnValue res; + + res = TEH_DB_run_transaction (connection, + "set extensions", + TEH_MT_REQUEST_OTHER, + &ret, + &set_extensions, + &sec); + + if (GNUNET_SYSERR == res) + goto CLEANUP; + } + + ret = TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + +CLEANUP: + for (unsigned int i = 0; i < sec.num_extensions; i++) + { + if (NULL != sec.extensions[i].manifest) + { + json_decref (sec.extensions[i].manifest); + } + } + GNUNET_free (sec.extensions); + return ret; +} + + +/* end of taler-exchange-httpd_management_management_post_extensions.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-global-fees.c b/src/exchange/taler-exchange-httpd_post-management-global-fees.c @@ -0,0 +1,261 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2021, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-global-fees.c + * @brief Handle request to add global fee details + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Closure for the #add_fee transaction. + */ +struct AddFeeContext +{ + /** + * Fee's signature affirming the #TALER_SIGNATURE_MASTER_GLOBAL_FEES operation. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Starting period. + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * End of period. + */ + struct GNUNET_TIME_Timestamp end_time; + + /** + * Global fee amounts. + */ + struct TALER_GlobalFeeSet fees; + + /** + * When does an unmerged purse expire? + */ + struct GNUNET_TIME_Relative purse_timeout; + + /** + * When does an account without KYC expire? + */ + struct GNUNET_TIME_Relative kyc_timeout; + + /** + * When does an account history expire? + */ + struct GNUNET_TIME_Relative history_expiration; + + /** + * Number of free purses per account. + */ + uint32_t purse_account_limit; + +}; + + +/** + * Function implementing database transaction to add a fee. 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 closure with a `struct AddFeeContext` + * @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 +add_fee (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AddFeeContext *afc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_GlobalFeeSet fees; + struct GNUNET_TIME_Relative purse_timeout; + struct GNUNET_TIME_Relative history_expiration; + uint32_t purse_account_limit; + + qs = TEH_plugin->lookup_global_fee_by_time ( + TEH_plugin->cls, + afc->start_time, + afc->end_time, + &fees, + &purse_timeout, + &history_expiration, + &purse_account_limit); + 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, + "lookup global fee"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) + { + if ( (GNUNET_OK == + TALER_amount_is_valid (&fees.history)) && + (0 == + TALER_global_fee_set_cmp (&fees, + &afc->fees)) ) + { + /* this will trigger the 'success' response */ + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + else + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH, + NULL); + } + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->insert_global_fee ( + TEH_plugin->cls, + afc->start_time, + afc->end_time, + &afc->fees, + afc->purse_timeout, + afc->history_expiration, + afc->purse_account_limit, + &afc->master_sig); + 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 fee"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_management_post_global_fees ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct AddFeeContext afc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &afc.master_sig), + GNUNET_JSON_spec_timestamp ("fee_start", + &afc.start_time), + GNUNET_JSON_spec_timestamp ("fee_end", + &afc.end_time), + TALER_JSON_spec_amount ("history_fee", + TEH_currency, + &afc.fees.history), + TALER_JSON_spec_amount ("account_fee", + TEH_currency, + &afc.fees.account), + TALER_JSON_spec_amount ("purse_fee", + TEH_currency, + &afc.fees.purse), + GNUNET_JSON_spec_relative_time ("purse_timeout", + &afc.purse_timeout), + GNUNET_JSON_spec_relative_time ("history_expiration", + &afc.history_expiration), + GNUNET_JSON_spec_uint32 ("purse_account_limit", + &afc.purse_account_limit), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_global_fee_verify ( + afc.start_time, + afc.end_time, + &afc.fees, + afc.purse_timeout, + afc.history_expiration, + afc.purse_account_limit, + &TEH_master_public_key, + &afc.master_sig)) + { + /* signature invalid */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID, + NULL); + } + + { + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + + res = TEH_DB_run_transaction (connection, + "add global fee", + TEH_MT_REQUEST_OTHER, + &ret, + &add_fee, + &afc); + if (GNUNET_SYSERR == res) + return ret; + } + TEH_keys_update_states (); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_global_fees.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-keys.c b/src/exchange/taler-exchange-httpd_post-management-keys.c @@ -0,0 +1,574 @@ +/* + This file is part of TALER + Copyright (C) 2020-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-keys.c + * @brief Handle request to POST /management/keys + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Denomination signature provided. + */ +struct DenomSig +{ + /** + * Hash of a denomination public key. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Master signature for the @e h_denom_pub. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Fee structure for this key, as per our configuration. + */ + struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; + + /** + * The full public key. + */ + struct TALER_DenominationPublicKey denom_pub; + +}; + + +/** + * Signkey signature provided. + */ +struct SigningSig +{ + /** + * Online signing key of the exchange. + */ + struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * Master signature for the @e exchange_pub. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Our meta data on this key. + */ + struct TALER_EXCHANGEDB_SignkeyMetaData meta; + +}; + + +/** + * Closure for the #add_keys transaction. + */ +struct AddKeysContext +{ + + /** + * Array of @e nd_sigs denomination signatures. + */ + struct DenomSig *d_sigs; + + /** + * Array of @e ns_sigs signkey signatures. + */ + struct SigningSig *s_sigs; + + /** + * Our key state. + */ + struct TEH_KeyStateHandle *ksh; + + /** + * Length of the d_sigs array. + */ + unsigned int nd_sigs; + + /** + * Length of the n_sigs array. + */ + unsigned int ns_sigs; + +}; + + +/** + * Compare meta-data of two denomination keys for equality, + * except for the "serial" number. + * + * @param m1 meta data to compare to @a m2 + * @param m2 meta data to compare to @a m1 + * @return true if both are equal + */ +static bool +denomination_meta_cmp ( + const struct TALER_EXCHANGEDB_DenominationKeyMetaData *m1, + const struct TALER_EXCHANGEDB_DenominationKeyMetaData *m2) +{ + if ( (GNUNET_TIME_timestamp_cmp (m1->start, + !=, + m2->start)) || + (GNUNET_TIME_timestamp_cmp (m1->expire_withdraw, + !=, + m2->expire_withdraw)) || + (GNUNET_TIME_timestamp_cmp (m1->expire_deposit, + !=, + m2->expire_deposit)) || + (GNUNET_TIME_timestamp_cmp (m1->expire_legal, + !=, + m2->expire_legal)) ) + return false; + if (0 != + TALER_amount_cmp (&m1->value, + &m2->value)) + return false; + if (0 != + GNUNET_memcmp (&m1->fees, + &m2->fees)) + return false; + if (m1->age_mask.bits != + m2->age_mask.bits) + return false; + return true; +} + + +/** + * Compare meta-data of two signing keys for equality. + * + * @param m1 meta data to compare to @a m2 + * @param m2 meta data to compare to @a m1 + * @return true if both are equal + */ +static bool +signkey_meta_cmp ( + const struct TALER_EXCHANGEDB_SignkeyMetaData *m1, + const struct TALER_EXCHANGEDB_SignkeyMetaData *m2) +{ + if ( (GNUNET_TIME_timestamp_cmp (m1->start, + !=, + m2->start)) || + (GNUNET_TIME_timestamp_cmp (m1->expire_sign, + !=, + m2->expire_sign)) || + (GNUNET_TIME_timestamp_cmp (m1->expire_legal, + !=, + m2->expire_legal)) ) + return false; + return true; +} + + +/** + * Function implementing database transaction to add offline signing keys. + * 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 closure with a `struct AddKeysContext` + * @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 +add_keys (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AddKeysContext *akc = cls; + + /* activate all denomination keys */ + for (unsigned int i = 0; i<akc->nd_sigs; i++) + { + struct DenomSig *d = &akc->d_sigs[i]; + enum GNUNET_DB_QueryStatus qs; + struct TALER_EXCHANGEDB_DenominationKeyMetaData meta; + + /* For idempotency, check if the key is already active */ + qs = TEH_plugin->lookup_denomination_key ( + TEH_plugin->cls, + &d->h_denom_pub, + &meta); + 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, + "lookup denomination key"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + if (! denomination_meta_cmp (&d->meta, + &meta)) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_CONFLICTING_DENOMINATION_META_DATA, + "conflicting meta data previously set for the same denomination key"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Denomination key %s already active, skipping\n", + GNUNET_h2s (&d->h_denom_pub.hash)); + continue; /* skip, already known */ + } + + qs = TEH_plugin->add_denomination_key ( + TEH_plugin->cls, + &d->h_denom_pub, + &d->denom_pub, + &d->meta, + &d->master_sig); + 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, + "activate denomination key"); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Added offline signature for denomination `%s'\n", + GNUNET_h2s (&d->h_denom_pub.hash)); + GNUNET_assert (0 != qs); + } + + for (unsigned int i = 0; i<akc->ns_sigs; i++) + { + struct SigningSig *s = &akc->s_sigs[i]; + enum GNUNET_DB_QueryStatus qs; + struct TALER_EXCHANGEDB_SignkeyMetaData meta; + + qs = TEH_plugin->lookup_signing_key ( + TEH_plugin->cls, + &s->exchange_pub, + &meta); + 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, + "lookup signing key"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + if (! signkey_meta_cmp (&s->meta, + &meta)) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_CONFLICTING_SIGNKEY_META_DATA, + "conflicting meta data previously set for the same signing key"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signing key %s already active, skipping\n", + TALER_B2S (&s->exchange_pub)); + continue; /* skip, already known */ + } + qs = TEH_plugin->activate_signing_key ( + TEH_plugin->cls, + &s->exchange_pub, + &s->meta, + &s->master_sig); + 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, + "activate signing key"); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Added offline signature for signing key `%s'\n", + TALER_B2S (&s->exchange_pub)); + GNUNET_assert (0 != qs); + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* only 'success', so >=0, matters here */ +} + + +/** + * Clean up state in @a akc, but do not free @a akc itself + * + * @param[in,out] akc state to clean up + */ +static void +cleanup_akc (struct AddKeysContext *akc) +{ + for (unsigned int i = 0; i<akc->nd_sigs; i++) + { + struct DenomSig *d = &akc->d_sigs[i]; + + TALER_denom_pub_free (&d->denom_pub); + } + GNUNET_free (akc->d_sigs); + GNUNET_free (akc->s_sigs); +} + + +MHD_RESULT +TEH_handler_management_post_keys ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct AddKeysContext akc = { 0 }; + const json_t *denom_sigs; + const json_t *signkey_sigs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("denom_sigs", + &denom_sigs), + GNUNET_JSON_spec_array_const ("signkey_sigs", + &signkey_sigs), + GNUNET_JSON_spec_end () + }; + MHD_RESULT ret; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received POST /management/keys request\n"); + + akc.ksh = TEH_keys_get_state_for_management_only (); /* may start its own transaction, thus must be done here, before we run ours! */ + if (NULL == akc.ksh) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + "no key state (not even for management)"); + } + + akc.nd_sigs = json_array_size (denom_sigs); + akc.d_sigs = GNUNET_new_array (akc.nd_sigs, + struct DenomSig); + for (unsigned int i = 0; i<akc.nd_sigs; i++) + { + struct DenomSig *d = &akc.d_sigs[i]; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &d->master_sig), + GNUNET_JSON_spec_fixed_auto ("h_denom_pub", + &d->h_denom_pub), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + json_array_get (denom_sigs, + i), + ispec); + if (GNUNET_OK != res) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failure to handle /management/keys\n"); + cleanup_akc (&akc); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + + res = TEH_keys_load_fees (akc.ksh, + &d->h_denom_pub, + &d->denom_pub, + &d->meta); + switch (res) + { + case GNUNET_SYSERR: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + GNUNET_h2s (&d->h_denom_pub.hash)); + cleanup_akc (&akc); + return ret; + case GNUNET_NO: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + GNUNET_h2s (&d->h_denom_pub.hash)); + cleanup_akc (&akc); + return ret; + case GNUNET_OK: + break; + } + /* check signature is valid */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_denom_validity_verify ( + &d->h_denom_pub, + d->meta.start, + d->meta.expire_withdraw, + d->meta.expire_deposit, + d->meta.expire_legal, + &d->meta.value, + &d->meta.fees, + &TEH_master_public_key, + &d->master_sig)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID, + GNUNET_h2s (&d->h_denom_pub.hash)); + cleanup_akc (&akc); + return ret; + } + } + + akc.ns_sigs = json_array_size (signkey_sigs); + akc.s_sigs = GNUNET_new_array (akc.ns_sigs, + struct SigningSig); + for (unsigned int i = 0; i<akc.ns_sigs; i++) + { + struct SigningSig *s = &akc.s_sigs[i]; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &s->master_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &s->exchange_pub), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + json_array_get (signkey_sigs, + i), + ispec); + if (GNUNET_OK != res) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failure to handle /management/keys\n"); + cleanup_akc (&akc); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + res = TEH_keys_get_timing (&s->exchange_pub, + &s->meta); + switch (res) + { + case GNUNET_SYSERR: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + TALER_B2S (&s->exchange_pub)); + cleanup_akc (&akc); + return ret; + case GNUNET_NO: + /* For idempotency, check if the key is already active */ + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN, + TALER_B2S (&s->exchange_pub)); + cleanup_akc (&akc); + return ret; + case GNUNET_OK: + break; + } + + /* check signature is valid */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_signkey_validity_verify ( + &s->exchange_pub, + s->meta.start, + s->meta.expire_sign, + s->meta.expire_legal, + &TEH_master_public_key, + &s->master_sig)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID, + TALER_B2S (&s->exchange_pub)); + cleanup_akc (&akc); + return ret; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received %u denomination and %u signing key signatures\n", + akc.nd_sigs, + akc.ns_sigs); + { + enum GNUNET_GenericReturnValue res; + + res = TEH_DB_run_transaction (connection, + "add keys", + TEH_MT_REQUEST_OTHER, + &ret, + &add_keys, + &akc); + cleanup_akc (&akc); + if (GNUNET_SYSERR == res) + return ret; + } + TEH_keys_update_states (); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_management_post_keys.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-partners.c b/src/exchange/taler-exchange-httpd_post-management-partners.c @@ -0,0 +1,132 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-partners.c + * @brief Handle request to add exchange partner + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" + + +MHD_RESULT +TEH_handler_management_partners ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct TALER_MasterPublicKeyP partner_pub; + struct GNUNET_TIME_Timestamp start_date; + struct GNUNET_TIME_Timestamp end_date; + struct GNUNET_TIME_Relative wad_frequency; + struct TALER_Amount wad_fee; + const char *partner_base_url; + struct TALER_MasterSignatureP master_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("partner_pub", + &partner_pub), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + TALER_JSON_spec_web_url ("partner_base_url", + &partner_base_url), + TALER_JSON_spec_amount ("wad_fee", + TEH_currency, + &wad_fee), + GNUNET_JSON_spec_timestamp ("start_date", + &start_date), + GNUNET_JSON_spec_timestamp ("end_date", + &end_date), + GNUNET_JSON_spec_relative_time ("wad_frequency", + &wad_frequency), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_partner_details_verify ( + &partner_pub, + start_date, + end_date, + wad_frequency, + &wad_fee, + partner_base_url, + &TEH_master_public_key, + &master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID, + NULL); + } + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->insert_partner (TEH_plugin->cls, + &partner_pub, + start_date, + end_date, + wad_frequency, + &wad_fee, + partner_base_url, + &master_sig); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "add_partner"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* FIXME-#7271: check for idempotency! */ + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT, + NULL); + } + } + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_partners.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-signkeys-EXCHANGE_PUB-revoke.c b/src/exchange/taler-exchange-httpd_post-management-signkeys-EXCHANGE_PUB-revoke.c @@ -0,0 +1,93 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-signkeys-EXCHANGE_PUB-revoke.c + * @brief Handle exchange online signing key revocation requests. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +MHD_RESULT +TEH_handler_management_signkeys_EP_revoke ( + struct MHD_Connection *connection, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const json_t *root) +{ + struct TALER_MasterSignatureP master_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_signkey_revoke_verify (exchange_pub, + &TEH_master_public_key, + &master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID, + NULL); + } + qs = TEH_plugin->insert_signkey_revocation (TEH_plugin->cls, + exchange_pub, + &master_sig); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "signkey revocation"); + } + TEH_keys_update_states (); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_signkey_HDP_revoke.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-wire-disable.c b/src/exchange/taler-exchange-httpd_post-management-wire-disable.c @@ -0,0 +1,207 @@ +/* + This file is part of TALER + Copyright (C) 2020-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-wire-disable.c + * @brief Handle request to disable wire account. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for the #del_wire transaction. + */ +struct DelWireContext +{ + /** + * Master signature affirming the WIRE DEL operation + * (includes timestamp). + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Payto:// URI this is about. + */ + struct TALER_FullPayto payto_uri; + + /** + * Timestamp for checking against replay attacks. + */ + struct GNUNET_TIME_Timestamp validity_end; + +}; + + +/** + * Function implementing database transaction to del an wire. 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 closure with a `struct DelWireContext` + * @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 +del_wire (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct DelWireContext *awc = cls; + struct GNUNET_TIME_Timestamp last_date; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->lookup_wire_timestamp (TEH_plugin->cls, + awc->payto_uri, + &last_date); + 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, + "lookup wire"); + return qs; + } + if (GNUNET_TIME_timestamp_cmp (last_date, + >, + awc->validity_end)) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + qs = TEH_plugin->update_wire (TEH_plugin->cls, + awc->payto_uri, + NULL, + NULL, + NULL, + NULL, + NULL, + awc->validity_end, + NULL, + NULL, + 0, + false); + if (qs < 0) + { + GNUNET_break (0); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "del wire"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_management_post_wire_disable ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct DelWireContext awc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig_del", + &awc.master_sig), + TALER_JSON_spec_full_payto_uri ("payto_uri", + &awc.payto_uri), + GNUNET_JSON_spec_timestamp ("validity_end", + &awc.validity_end), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + if (GNUNET_OK != + TALER_exchange_offline_wire_del_verify ( + awc.payto_uri, + awc.validity_end, + &TEH_master_public_key, + &awc.master_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID, + NULL); + } + + { + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + + res = TEH_DB_run_transaction (connection, + "del wire", + TEH_MT_REQUEST_OTHER, + &ret, + &del_wire, + &awc); + if (GNUNET_SYSERR == res) + return ret; + } + TEH_wire_update_state (); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_wire_disable.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-wire-fee.c b/src/exchange/taler-exchange-httpd_post-management-wire-fee.c @@ -0,0 +1,230 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2021, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-wire-fee.c + * @brief Handle request to add wire fee details + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for the #add_fee transaction. + */ +struct AddFeeContext +{ + /** + * Fee's signature affirming the #TALER_SIGNATURE_MASTER_WIRE_FEES operation. + */ + struct TALER_MasterSignatureP master_sig; + + /** + * Wire method this is about. + */ + const char *wire_method; + + /** + * Starting period. + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * End of period. + */ + struct GNUNET_TIME_Timestamp end_time; + + /** + * Wire fee amounts. + */ + struct TALER_WireFeeSet fees; + +}; + + +/** + * Function implementing database transaction to add a fee. 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 closure with a `struct AddFeeContext` + * @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 +add_fee (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AddFeeContext *afc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_WireFeeSet fees; + + qs = TEH_plugin->lookup_wire_fee_by_time ( + TEH_plugin->cls, + afc->wire_method, + afc->start_time, + afc->end_time, + &fees); + 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, + "lookup wire fee"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) + { + if ( (GNUNET_OK == + TALER_amount_is_valid (&fees.wire)) && + (0 == + TALER_wire_fee_set_cmp (&fees, + &afc->fees)) ) + { + /* this will trigger the 'success' response */ + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + else + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH, + NULL); + } + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->insert_wire_fee ( + TEH_plugin->cls, + afc->wire_method, + afc->start_time, + afc->end_time, + &afc->fees, + &afc->master_sig); + 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 fee"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_management_post_wire_fees ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct AddFeeContext afc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_sig", + &afc.master_sig), + GNUNET_JSON_spec_string ("wire_method", + &afc.wire_method), + GNUNET_JSON_spec_timestamp ("fee_start", + &afc.start_time), + GNUNET_JSON_spec_timestamp ("fee_end", + &afc.end_time), + TALER_JSON_spec_amount ("wire_fee", + TEH_currency, + &afc.fees.wire), + TALER_JSON_spec_amount ("closing_fee", + TEH_currency, + &afc.fees.closing), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_offline_wire_fee_verify ( + afc.wire_method, + afc.start_time, + afc.end_time, + &afc.fees, + &TEH_master_public_key, + &afc.master_sig)) + { + /* signature invalid */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID, + NULL); + } + + { + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + + res = TEH_DB_run_transaction (connection, + "add wire fee", + TEH_MT_REQUEST_OTHER, + &ret, + &add_fee, + &afc); + if (GNUNET_SYSERR == res) + return ret; + } + TEH_wire_update_state (); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_wire_fees.c */ diff --git a/src/exchange/taler-exchange-httpd_post-management-wire.c b/src/exchange/taler-exchange-httpd_post-management-wire.c @@ -0,0 +1,346 @@ +/* + This file is part of TALER + Copyright (C) 2020-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-management-wire.c + * @brief Handle request to add wire account. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_signatures.h" +#include "taler-exchange-httpd_management.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for the #add_wire transaction. + */ +struct AddWireContext +{ + /** + * Master signature affirming the WIRE ADD operation + * (includes timestamp). + */ + struct TALER_MasterSignatureP master_sig_add; + + /** + * Master signature to share with clients affirming the + * wire details of the bank. + */ + struct TALER_MasterSignatureP master_sig_wire; + + /** + * Payto:// URI this is about. + */ + struct TALER_FullPayto payto_uri; + + /** + * (optional) address of a conversion service for this account. + */ + const char *conversion_url; + + /** + * (optional) address of an open banking gateway service for this account. + */ + const char *open_banking_gateway; + + /** + * (optional) address of a wire transfer gateway service for this account. + */ + const char *wire_transfer_gateway; + + /** + * Restrictions imposed when crediting this account. + */ + const json_t *credit_restrictions; + + /** + * Restrictions imposed when debiting this account. + */ + const json_t *debit_restrictions; + + /** + * Timestamp for checking against replay attacks. + */ + struct GNUNET_TIME_Timestamp validity_start; + + /** + * Label to use for this bank. Default is empty. + */ + const char *bank_label; + + /** + * Priority of the bank in the list. Default 0. + */ + int64_t priority; + +}; + + +/** + * Function implementing database transaction to add an wire. 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 closure with a `struct AddWireContext` + * @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 +add_wire (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct AddWireContext *awc = cls; + struct GNUNET_TIME_Timestamp last_date; + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->lookup_wire_timestamp (TEH_plugin->cls, + awc->payto_uri, + &last_date); + 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, + "lookup wire"); + return qs; + } + if ( (0 < qs) && + (GNUNET_TIME_timestamp_cmp (last_date, + >, + awc->validity_start)) ) + { + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + qs = TEH_plugin->insert_wire (TEH_plugin->cls, + awc->payto_uri, + awc->conversion_url, + awc->open_banking_gateway, + awc->wire_transfer_gateway, + awc->debit_restrictions, + awc->credit_restrictions, + awc->validity_start, + &awc->master_sig_wire, + awc->bank_label, + awc->priority); + else + qs = TEH_plugin->update_wire (TEH_plugin->cls, + awc->payto_uri, + awc->conversion_url, + awc->open_banking_gateway, + awc->wire_transfer_gateway, + awc->debit_restrictions, + awc->credit_restrictions, + awc->validity_start, + &awc->master_sig_wire, + awc->bank_label, + awc->priority, + true); + if (qs < 0) + { + GNUNET_break (0); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "add wire"); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_management_post_wire ( + struct MHD_Connection *connection, + const json_t *root) +{ + struct AddWireContext awc = { + .conversion_url = NULL + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_full_payto_uri ("payto_uri", + &awc.payto_uri), + GNUNET_JSON_spec_fixed_auto ("master_sig_wire", + &awc.master_sig_wire), + GNUNET_JSON_spec_fixed_auto ("master_sig_add", + &awc.master_sig_add), + GNUNET_JSON_spec_timestamp ("validity_start", + &awc.validity_start), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("conversion_url", + &awc.conversion_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("open_banking_gateway", + &awc.open_banking_gateway), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("wire_transfer_gateway", + &awc.wire_transfer_gateway), + NULL), + GNUNET_JSON_spec_array_const ("credit_restrictions", + &awc.credit_restrictions), + GNUNET_JSON_spec_array_const ("debit_restrictions", + &awc.debit_restrictions), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("bank_label", + &awc.bank_label), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_int64 ("priority", + &awc.priority), + NULL), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + return MHD_YES; /* failure */ + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + { + char *msg = TALER_payto_validate (awc.payto_uri); + + if (NULL != msg) + { + MHD_RESULT ret; + + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + msg); + GNUNET_JSON_parse_free (spec); + GNUNET_free (msg); + return ret; + } + } + if (GNUNET_OK != + TALER_exchange_offline_wire_add_verify ( + awc.payto_uri, + awc.conversion_url, + awc.open_banking_gateway, + awc.wire_transfer_gateway, + awc.debit_restrictions, + awc.credit_restrictions, + awc.validity_start, + &TEH_master_public_key, + &awc.master_sig_add)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID, + NULL); + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_exchange_wire_signature_check ( + awc.payto_uri, + awc.conversion_url, + awc.open_banking_gateway, + awc.wire_transfer_gateway, + awc.debit_restrictions, + awc.credit_restrictions, + &TEH_master_public_key, + &awc.master_sig_wire)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID, + NULL); + } + { + char *wire_method; + + wire_method = TALER_payto_get_method (awc.payto_uri.full_payto); + if (NULL == wire_method) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "payto:// URI `%s' is malformed\n", + awc.payto_uri.full_payto); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + GNUNET_free (wire_method); + } + + { + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + + res = TEH_DB_run_transaction (connection, + "add wire", + TEH_MT_REQUEST_OTHER, + &ret, + &add_wire, + &awc); + GNUNET_JSON_parse_free (spec); + if (GNUNET_SYSERR == res) + return ret; + } + TEH_wire_update_state (); + return TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-exchange-httpd_management_wire.c */ diff --git a/src/exchange/taler-exchange-httpd_post-melt.c b/src/exchange/taler-exchange-httpd_post-melt.c @@ -0,0 +1,1864 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-melt.c + * @brief Handle /melt requests + * @note This endpoint is active since vDOLDPLUS of the protocol API + * @author Özgür Kesim + */ + +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler-exchange-httpd.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_post-melt.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler/taler_util.h" + +/** + * The different type of errors that might occur, sorted by name. + * Some of them require idempotency checks, which are marked + * in @e idempotency_check_required below. + */ +enum MeltError +{ + MELT_ERROR_NONE = 0, + MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID, + MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + MELT_ERROR_AMOUNT_OVERFLOW, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, + MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT, + MELT_ERROR_BLINDING_SEED_REQUIRED, + MELT_ERROR_COIN_CIPHER_MISMATCH, + MELT_COIN_CONFLICTING_DENOMINATION_KEY, + MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE, + MELT_ERROR_COIN_SIGNATURE_INVALID, + MELT_ERROR_COIN_UNKNOWN, + MELT_ERROR_CONFIRMATION_SIGN, + MELT_ERROR_CRYPTO_HELPER, + MELT_ERROR_DB_FETCH_FAILED, + MELT_ERROR_DB_INVARIANT_FAILURE, + MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE, + MELT_ERROR_DB_PREFLIGHT_FAILURE, + MELT_ERROR_DENOMINATION_EXPIRED, + MELT_ERROR_DENOMINATION_KEY_UNKNOWN, + MELT_ERROR_DENOMINATION_REVOKED, + MELT_ERROR_DENOMINATION_SIGN, + MELT_ERROR_DENOMINATION_SIGNATURE_INVALID, + MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + MELT_ERROR_DUPLICATE_PLANCHET, + MELT_ERROR_DUPLICATE_TRANSFER_PUB, + MELT_ERROR_INSUFFICIENT_FUNDS, + MELT_ERROR_KEYS_MISSING, + MELT_ERROR_FEES_EXCEED_CONTRIBUTION, + MELT_ERROR_NONCE_RESUSE, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, +}; + +/** + * With the bits set in this value will be mark the errors + * that require a check for idempotency before actually + * returning an error. + */ +static const uint64_t idempotency_check_required = + 0 + | (1 << MELT_ERROR_DENOMINATION_EXPIRED) + | (1 << MELT_ERROR_DENOMINATION_KEY_UNKNOWN) + | (1 << MELT_ERROR_DENOMINATION_REVOKED) + | (1 << MELT_ERROR_INSUFFICIENT_FUNDS) /* TODO: is this still correct? Compare exchange_do_refresh.sql */ + | (1 << MELT_ERROR_KEYS_MISSING); + +#define IDEMPOTENCY_CHECK_REQUIRED(error) \ + (0 != (idempotency_check_required & (1 << (error)))) + +/** + * Context for a /melt request + */ +struct MeltContext +{ + + /** + * This struct is kept in a DLL. + */ + struct MeltContext *prev; + struct MeltContext *next; + + /** + * Processing phase we are in. + * The ordering here partially matters, as we progress through + * them by incrementing the phase in the happy path. + */ + enum MeltPhase + { + MELT_PHASE_PARSE, + MELT_PHASE_CHECK_MELT_VALID, + MELT_PHASE_CHECK_KEYS, + MELT_PHASE_CHECK_COIN_SIGNATURE, + MELT_PHASE_PREPARE_TRANSACTION, + MELT_PHASE_RUN_TRANSACTION, + MELT_PHASE_GENERATE_REPLY_SUCCESS, + MELT_PHASE_GENERATE_REPLY_ERROR, + MELT_PHASE_RETURN_NO, + MELT_PHASE_RETURN_YES, + } phase; + + + /** + * Request context + */ + const struct TEH_RequestContext *rc; + + /** + * Current time for the DB transaction. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * The current key state + */ + struct TEH_KeyStateHandle *ksh; + + /** + * The melted coin's denomination key + */ + struct TEH_DenominationKey *melted_coin_denom; + + /** + * Set to true if this coin's denomination was revoked and the operation + * is thus only allowed for zombie coins where the transaction + * history includes a #TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP. + * + * TODO: find a better terminology. The sentences in the comments containing + * "zombie" make semantically _no sense_! + */ + bool zombie_required; + + /** + * We already checked and noticed that the coin is known. Hence we + * can skip the "ensure_coin_known" step of the transaction. + */ + bool coin_is_known; + + /** + * UUID of the coin in the known_coins table. + */ + uint64_t known_coin_id; + + /** + * Captures all parameters provided in the JSON request + */ + struct + { + + /** + * All fields (from the request or computed) + * that we persist in the database. + */ + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh; + + /** + * In some error cases we check for idempotency. + * If we find an entry in the database, we mark this here. + */ + bool is_idempotent; + + /** + * In some error conditions the request is checked + * for idempotency and the result from the database + * is stored here. + */ + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh_idem; + + /** + * True if @e blinding_seed is missing in the request + */ + bool no_blinding_seed; + + /** + * Array @e persis.num_coins of hashes of the public keys + * of the denominations to refresh. + */ + struct TALER_DenominationHashP *denoms_h; + + /** + * Array of @e refresh.num_coins blinded coin planchets, arranged + * in runs of @e refresh.num_coins coins, [0..num_coins)..[0..num_coins), + * one for each kappa value. + */ + struct TALER_BlindedPlanchet *planchets[TALER_CNC_KAPPA]; + + /** + * @since vDOLDPLUS + * #TALER_CNC_KAPPA arrays of @e refresh.num_coins transfer public keys + * in runs of @e num_coins coins, [0..num_coins)..[0..num_coins), + * one for each kappa value. + * + * MAYBE null. If the client has NOT provided the transfer_pubs in the request, + * @e refresh.is_v27_refresh will be true. + * + * TODO: Once v27 clients are gone, this MUST NOT be nulls. + */ + struct TALER_TransferPublicKeyP *transfer_pubs[TALER_CNC_KAPPA]; + + /** + * #TALER_CNC_KAPPA hashes of the batches of @e num_coins coins. + */ + struct TALER_KappaHashBlindedPlanchetsP kappa_planchets_h; + + /** + * Array @e withdraw.num_r_pubs of indices into @e denoms_h + * of CS denominations. + */ + uint32_t *cs_indices; + + /** + * Total (over all coins) amount (excluding fee) committed for the refresh + */ + struct TALER_Amount amount; + + } request; + + /** + * Errors occurring during evaluation of the request are captured in this + * struct. In phase WITHDRAW_PHASE_GENERATE_REPLY_ERROR an appropriate error + * message is prepared and sent to the client. + */ + struct + { + /* The (internal) error code */ + enum MeltError code; + + /** + * Some errors require details to be sent to the client. + * These are captured in this union. + * Each field is named according to the error that is using it, except + * commented otherwise. + */ + union + { + const char *request_parameter_malformed; + + /** + * For all errors related to a particular denomination, i.e. + * #MELT_ERROR_DENOMINATION_KEY_UNKNOWN, + * #MELT_ERROR_DENOMINATION_EXPIRED, + * #MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + * #MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + * we use this one field. + */ + struct TALER_DenominationHashP denom_h; + + const char *db_fetch_context; + + enum TALER_ErrorCode ec_confirmation_sign; + + enum TALER_ErrorCode ec_denomination_sign; + + /* remaining value of the coin */ + struct TALER_Amount insufficient_funds; + + } details; + } error; +}; + +/** + * The following macros set the given error code, + * set the phase to Melt_PHASE_GENERATE_REPLY_ERROR, + * and optionally set the given field (with an optionally given value). + */ +#define SET_ERROR(mc, ec) \ + do \ + { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ + (mc)->error.code = (ec); \ + (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_FIELD(mc, ec, field) \ + do \ + { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ + (mc)->error.code = (ec); \ + (mc)->error.details.field = (field); \ + (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_DETAIL(mc, ec, field, value) \ + do \ + { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ + (mc)->error.code = (ec); \ + (mc)->error.details.field = (value); \ + (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) + + +/** + * All melt context is kept in a DLL. + */ +static struct MeltContext *mc_head; +static struct MeltContext *mc_tail; + +void +TEH_melt_cleanup () +{ + struct MeltContext *mc; + + while (NULL != (mc = mc_head)) + { + GNUNET_CONTAINER_DLL_remove (mc_head, + mc_tail, + mc); + MHD_resume_connection (mc->rc->connection); + } +} + + +/** + * Terminate the main loop by returning the final result. + * + * @param[in,out] mc context to update phase for + * @param mres MHD status to return + */ +static void +finish_loop (struct MeltContext *mc, + MHD_RESULT mres) +{ + mc->phase = (MHD_YES == mres) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; +} + + +/** + * Free information in @a re, but not @a re itself. + * + * @param[in] re refresh data to free + */ +static void +free_refresh (struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *re) +{ + if (NULL != re->denom_sigs) + { + for (size_t i = 0; i<re->num_coins; i++) + TALER_blinded_denom_sig_free (&re->denom_sigs[i]); + GNUNET_free (re->denom_sigs); + } + GNUNET_free (re->cs_r_values); + GNUNET_free (re->denom_serials); + GNUNET_free (re->denom_pub_hashes); + TALER_denom_sig_free (&re->coin.denom_sig); +} + + +/** + * Cleanup routine for melt request. + * The function is called upon completion of the request + * that should clean up @a rh_ctx. + * + * @param rc request context to clean up + */ +static void +clean_melt_rc (struct TEH_RequestContext *rc) +{ + struct MeltContext *mc = rc->rh_ctx; + + GNUNET_free (mc->request.denoms_h); + for (uint8_t k = 0; k<TALER_CNC_KAPPA; k++) + { + for (size_t i = 0; i<mc->request.refresh.num_coins; i++) + TALER_blinded_planchet_free (&mc->request.planchets[k][i]); + GNUNET_free (mc->request.planchets[k]); + if (! mc->request.refresh.is_v27_refresh) + GNUNET_free (mc->request.transfer_pubs[k]); + } + free_refresh (&mc->request.refresh); + if (mc->request.is_idempotent) + free_refresh (&mc->request.refresh_idem); + GNUNET_free (mc->request.cs_indices); + GNUNET_free (mc); +} + + +/** + * Creates a new context for the incoming melt request + * + * @param mc melt request context + * @param root json body of the request + */ +static void +phase_parse_request ( + struct MeltContext *mc, + const json_t *root) +{ + const json_t *j_denoms_h; + const json_t *j_coin_evs; + const json_t *j_transfer_pubs; + enum GNUNET_GenericReturnValue res; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("old_coin_pub", + &mc->request.refresh.coin.coin_pub), + GNUNET_JSON_spec_fixed_auto ("old_denom_pub_h", + &mc->request.refresh.coin.denom_pub_hash), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("old_age_commitment_h", + &mc->request.refresh.coin.h_age_commitment), + &mc->request.refresh.coin.no_age_commitment), + TALER_JSON_spec_denom_sig ("old_denom_sig", + &mc->request.refresh.coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("refresh_seed", + &mc->request.refresh.refresh_seed), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("blinding_seed", + &mc->request.refresh.blinding_seed), + &mc->request.refresh.no_blinding_seed), + TALER_JSON_spec_amount ("value_with_fee", + TEH_currency, + &mc->request.refresh.amount_with_fee), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("transfer_pubs", + &j_transfer_pubs), + &mc->request.refresh.is_v27_refresh), + GNUNET_JSON_spec_array_const ("denoms_h", + &j_denoms_h), + GNUNET_JSON_spec_array_const ("coin_evs", + &j_coin_evs), + GNUNET_JSON_spec_fixed_auto ("confirm_sig", + &mc->request.refresh.coin_sig), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + root, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + + /* validate array size */ + GNUNET_static_assert ( + TALER_MAX_COINS < INT_MAX / TALER_CNC_KAPPA); + + mc->request.refresh.num_coins = json_array_size (j_denoms_h); + if (0 == mc->request.refresh.num_coins) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "denoms_h must not be empty"); + return; + } + if (TALER_MAX_COINS < mc->request.refresh.num_coins) + { + /** + * The wallet had committed to more than the maximum coins allowed, the + * reserve has been charged, but now the user can not melt any money + * from it. Note that the user can't get their money back in this case! + */ + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "maximum number of coins that can be refreshed has been exceeded"); + return; + } + if (TALER_CNC_KAPPA != json_array_size (j_coin_evs)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "coin_evs must be an array of length "TALER_CNC_KAPPA_STR); + return; + } + if (! mc->request.refresh.is_v27_refresh && + (TALER_CNC_KAPPA != json_array_size (j_transfer_pubs))) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "transfer_pubs must be an array of length "TALER_CNC_KAPPA_STR); + return; + } + + + /* Extract the denomination hashes */ + { + size_t idx; + json_t *value; + + mc->request.denoms_h + = GNUNET_new_array (mc->request.refresh.num_coins, + struct TALER_DenominationHashP); + + json_array_foreach (j_denoms_h, idx, value) { + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &mc->request.denoms_h[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + value, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + } + } + + /* Parse blinded envelopes. */ + { + json_t *j_kappa_planchets; + size_t kappa; + struct GNUNET_HashContext *ctx; + + /* ctx to calculate the planchet_h */ + ctx = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != ctx); + + json_array_foreach (j_coin_evs, kappa, j_kappa_planchets) + { + json_t *j_cev; + size_t idx; + + if (mc->request.refresh.num_coins != json_array_size (j_kappa_planchets)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "coin_evs[i][] size"); + return; + } + + mc->request.planchets[kappa] = + GNUNET_new_array (mc->request.refresh.num_coins, + struct TALER_BlindedPlanchet); + + json_array_foreach (j_kappa_planchets, idx, j_cev) + { + /* Now parse the individual envelopes and calculate the hash of + * the commitment along the way. */ + struct GNUNET_JSON_Specification kspec[] = { + TALER_JSON_spec_blinded_planchet (NULL, + &mc->request.planchets[kappa][idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + j_cev, + kspec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + } + /* Save the hash of the batch of planchets for index kappa */ + TALER_wallet_blinded_planchets_hash ( + mc->request.refresh.num_coins, + mc->request.planchets[kappa], + mc->request.denoms_h, + &mc->request.kappa_planchets_h.tuple[kappa]); + GNUNET_CRYPTO_hash_context_read ( + ctx, + &mc->request.kappa_planchets_h.tuple[kappa], + sizeof(mc->request.kappa_planchets_h.tuple[kappa])); + } + /* Finally calculate the total hash over all planchets */ + GNUNET_CRYPTO_hash_context_finish ( + ctx, + &mc->request.refresh.planchets_h.hash); + } + /* Check for duplicate planchets. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol + */ + { + size_t max_idx = TALER_CNC_KAPPA * mc->request.refresh.num_coins; + + for (size_t I = 0; I < max_idx - 1; I++) + { + size_t ki = I / mc->request.refresh.num_coins; + size_t ni = I % mc->request.refresh.num_coins; + + for (size_t J = I + 1; J < max_idx; J++) + { + size_t kj = J / mc->request.refresh.num_coins; + size_t nj = J % mc->request.refresh.num_coins; + + if (0 == TALER_blinded_planchet_cmp ( + &mc->request.planchets[ki][ni], + &mc->request.planchets[kj][nj])) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_DUPLICATE_TRANSFER_PUB); + return; + } + } + } + } + + /* Parse the transfer public keys, if applicable */ + if (! mc->request.refresh.is_v27_refresh) + { + json_t *j_ktp; + size_t kappa; + size_t max_idx; + + json_array_foreach (j_transfer_pubs, kappa, j_ktp) + { + if (mc->request.refresh.num_coins != + json_array_size (j_ktp)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "transfer_pubs[i][] size"); + return; + } + + mc->request.transfer_pubs[kappa] = + GNUNET_new_array (mc->request.refresh.num_coins, + struct TALER_TransferPublicKeyP); + + /* Parse the batch of @e num_coins transfer public keys + * at index kappa */ + { + struct GNUNET_JSON_Specification ktp_spec[] = { + TALER_JSON_spec_array_fixed (NULL, + mc->request.refresh.num_coins, + mc->request.transfer_pubs[kappa], + sizeof(*mc->request.transfer_pubs[kappa]) + ), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + j_ktp, + ktp_spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + } + } + /* Check for duplicate transfer public keys. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol + */ + max_idx = TALER_CNC_KAPPA * mc->request.refresh.num_coins; + + for (size_t I = 0; I < max_idx - 1; I++) + { + size_t ki = I / mc->request.refresh.num_coins; + size_t ni = I % mc->request.refresh.num_coins; + + for (size_t J = I + 1; J < max_idx; J++) + { + size_t kj = J / mc->request.refresh.num_coins; + size_t nj = J % mc->request.refresh.num_coins; + + if (0 == GNUNET_memcmp ( + &mc->request.transfer_pubs[ki][ni], + &mc->request.transfer_pubs[kj][nj])) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_DUPLICATE_TRANSFER_PUB); + return; + } + } + } + } + + mc->ksh = TEH_keys_get_state (); + if (NULL == mc->ksh) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_KEYS_MISSING); + return; + } + mc->phase = MELT_PHASE_CHECK_MELT_VALID; +} + + +/** + * Check if the given denomination is still or already valid, has not been + * revoked and potentically supports age restriction. + * + * @param[in,out] mc context for the melt operation + * @param denom_h Hash of the denomination key to check + * @param[out] pdk denomination key found, might be NULL + * @return #GNUNET_OK when denomation was found and valid, + * #GNUNET_NO when denomination is not valid at this time + * #GNUNET_SYSERR otherwise (denomination invalid), with finish_loop called. + */ +static enum GNUNET_GenericReturnValue +find_denomination ( + struct MeltContext *mc, + const struct TALER_DenominationHashP *denom_h, + struct TEH_DenominationKey **pdk) +{ + struct TEH_DenominationKey *dk; + + *pdk = NULL; + GNUNET_assert (NULL != mc->ksh); + dk = TEH_keys_denomination_by_hash_from_state (mc->ksh, + denom_h, + NULL, + NULL); + if (NULL == dk) + { + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DENOMINATION_KEY_UNKNOWN, + denom_h, + *denom_h); + return GNUNET_SYSERR; + } + *pdk = dk; + + if (GNUNET_TIME_absolute_is_past ( + dk->meta.expire_withdraw.abs_time)) + { + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DENOMINATION_EXPIRED, + denom_h, + *denom_h); + /** + * Note that we return GNUNET_NO here. + * This way phase_check_melt_valid can react + * to it as a non-error case and do the zombie check. + */ + return GNUNET_NO; + } + + if (GNUNET_TIME_absolute_is_future ( + dk->meta.start.abs_time)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + denom_h, + *denom_h); + return GNUNET_SYSERR; + } + + if (dk->recoup_possible) + { + SET_ERROR (mc, + MELT_ERROR_DENOMINATION_REVOKED); + return GNUNET_SYSERR; + } + + /* In case of age melt, make sure that the denomination supports age restriction */ + if (! (mc->request.refresh.coin.no_age_commitment) && + (0 == dk->denom_pub.age_mask.bits)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + denom_h, + *denom_h); + return GNUNET_SYSERR; + } + if ((mc->request.refresh.coin.no_age_commitment) && + (0 != dk->denom_pub.age_mask.bits)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * Check if the given array of hashes of denomination_keys + * - belong to valid denominations + * - calculate the total amount of the denominations including fees + * for melt. + * + * @param mc context of the melt to check keys for + */ +static void +phase_check_keys ( + struct MeltContext *mc) +{ + bool is_cs_denom[mc->request.refresh.num_coins]; + + memset (is_cs_denom, + 0, + sizeof(is_cs_denom)); + + mc->request.refresh.denom_serials = + GNUNET_new_array (mc->request.refresh.num_coins, + uint64_t); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &mc->request.amount)); + + /* Calculate the total value and withdraw fees for the fresh coins */ + for (size_t i = 0; i < mc->request.refresh.num_coins; i++) + { + struct TEH_DenominationKey *dk; + + if (GNUNET_OK != + find_denomination (mc, + &mc->request.denoms_h[i], + &dk)) + return; + + if (GNUNET_CRYPTO_BSA_CS == + dk->denom_pub.bsign_pub_key->cipher) + { + if (mc->request.refresh.no_blinding_seed) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_BLINDING_SEED_REQUIRED); + return; + } + mc->request.refresh.num_cs_r_values++; + is_cs_denom[i] = true; + } + /* 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 != + mc->request.planchets[k][i].blinded_message->cipher) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_CIPHER_MISMATCH); + return; + } + } + } + /* Accumulate the values */ + if (0 > TALER_amount_add (&mc->request.amount, + &mc->request.amount, + &dk->meta.value)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_OVERFLOW); + return; + } + /* Accumulate the withdraw fees for the fresh coins */ + if (0 > TALER_amount_add (&mc->request.amount, + &mc->request.amount, + &dk->meta.fees.withdraw)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); + return; + } + mc->request.refresh.denom_serials[i] = dk->meta.serial; + } + + /** + * Calculate the amount (with withdraw fee) plus refresh fee and + * compare with the value provided by the client in the request. + */ + { + struct TALER_Amount amount_with_fee; + + if (0 > TALER_amount_add (&amount_with_fee, + &mc->request.amount, + &mc->melted_coin_denom->meta.fees.refresh)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); + return; + } + + if (0 != TALER_amount_cmp (&amount_with_fee, + &mc->request.refresh.amount_with_fee)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT); + return; + } + } + + /* Save the indices of CS denominations */ + if (0 < mc->request.refresh.num_cs_r_values) + { + size_t j = 0; + + mc->request.cs_indices = GNUNET_new_array ( + mc->request.refresh.num_cs_r_values, + uint32_t); + + for (size_t i = 0; i < mc->request.refresh.num_coins; i++) + { + if (is_cs_denom[i]) + mc->request.cs_indices[j++] = i; + } + } + mc->phase++; +} + + +/** + * Check that the client signature authorizing the melt is valid. + * + * @param[in,out] mc request context to check + */ +static void +phase_check_coin_signature ( + struct MeltContext *mc) +{ + /* We can now compute the commitment */ + { + struct TALER_KappaHashBlindedPlanchetsP k_bps_h = {0}; + struct TALER_KappaTransferPublicKeys k_transfer_pubs = {0}; + + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + TALER_wallet_blinded_planchets_hash ( + mc->request.refresh.num_coins, + mc->request.planchets[k], + mc->request.denoms_h, + &k_bps_h.tuple[k]); + + if (! mc->request.refresh.is_v27_refresh) + { + k_transfer_pubs.num_transfer_pubs = mc->request.refresh.num_coins; + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + k_transfer_pubs.batch[k] = &mc->request.refresh.transfer_pubs[k]; + } + + TALER_refresh_get_commitment ( + &mc->request.refresh.rc, + &mc->request.refresh.refresh_seed, + mc->request.no_blinding_seed + ? NULL + : &mc->request.refresh.blinding_seed, + mc->request.refresh.is_v27_refresh + ? NULL + : &k_transfer_pubs, + &k_bps_h, + &mc->request.refresh.coin.coin_pub, + &mc->request.refresh.amount_with_fee); + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_melt_verify ( + &mc->request.refresh.amount_with_fee, + &mc->melted_coin_denom->meta.fees.refresh, + &mc->request.refresh.rc, + &mc->request.refresh.coin.denom_pub_hash, + &mc->request.refresh.coin.h_age_commitment, + &mc->request.refresh.coin.coin_pub, + &mc->request.refresh.coin_sig)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_SIGNATURE_INVALID); + return; + } + + mc->phase++; +} + + +/** + * Check for information about the melted coin's denomination, + * extracting its validity status and fee structure. + * Baseline: check if deposits/refreshes are generally + * simply still allowed for this denomination. + * + * @param mc parsed request information + */ +static void +phase_check_melt_valid (struct MeltContext *mc) +{ + enum MeltPhase current_phase = mc->phase; + /** + * Find the old coin's denomination. + * Note that we return only on GNUNET_SYSERR, + * because GNUNET_NO for the expired denomination + * will be handled below, with the zombie-check. + */ + if (GNUNET_SYSERR == + find_denomination (mc, + &mc->request.refresh.coin.denom_pub_hash, + &mc->melted_coin_denom)) + return; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Melted coin's denomination is worth %s\n", + TALER_amount2s (&mc->melted_coin_denom->meta.value)); + + /* sanity-check that "total melt amount > melt fee" */ + if (0 < + TALER_amount_cmp (&mc->melted_coin_denom->meta.fees.refresh, + &mc->request.refresh.amount_with_fee)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_FEES_EXCEED_CONTRIBUTION); + return; + } + + if (GNUNET_OK != + TALER_test_coin_valid (&mc->request.refresh.coin, + &mc->melted_coin_denom->denom_pub)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_DENOMINATION_SIGNATURE_INVALID); + return; + } + + /** + * find_denomination might have set the phase to + * produce an error, but we are still investigating. + * We reset the phase. + */ + mc->phase = current_phase; + mc->error.code = MELT_ERROR_NONE; + + if (GNUNET_TIME_absolute_is_past ( + mc->melted_coin_denom->meta.expire_deposit.abs_time)) + { + /** + * We are past deposit expiration time, but maybe this is a zombie? + */ + struct TALER_DenominationHashP denom_hash; + enum GNUNET_DB_QueryStatus qs; + + /* Check that the coin is dirty (we have seen it before), as we will + not just allow melting of a *fresh* coin where the denomination was + revoked (those must be recouped) */ + qs = TEH_plugin->get_coin_denomination ( + TEH_plugin->cls, + &mc->request.refresh.coin.coin_pub, + &mc->known_coin_id, + &denom_hash); + if (0 > qs) + { + /* There is no good reason for a serialization failure here: */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + SET_ERROR (mc, + MELT_ERROR_DB_FETCH_FAILED); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + /* We never saw this coin before, so _this_ justification is not OK. + * Note that the error was already set in find_denominations. */ + GNUNET_assert (MELT_ERROR_DENOMINATION_EXPIRED == + mc->error.code); + GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == + mc->phase); + return; + } + /* sanity check */ + if (0 != + GNUNET_memcmp (&denom_hash, + &mc->request.refresh.coin.denom_pub_hash)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_COIN_CONFLICTING_DENOMINATION_KEY, + denom_h, + denom_hash); + return; + } + /* Minor optimization: no need to run the + "ensure_coin_known" part of the transaction */ + mc->coin_is_known = true; + /* check later that zombie is satisfied */ + mc->zombie_required = true; + } + mc->phase++; +} + + +/** + * The request for melt was parsed successfully. + * Sign and persist the chosen blinded coins for the reveal step. + * + * @param mc The context for the current melt request + */ +static void +phase_prepare_transaction ( + struct MeltContext *mc) +{ + mc->request.refresh.denom_sigs + = GNUNET_new_array ( + mc->request.refresh.num_coins, + struct TALER_BlindedDenominationSignature); + mc->request.refresh.noreveal_index = + GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, + TALER_CNC_KAPPA); + + /* Choose and sign the coins */ + { + struct TEH_CoinSignData csds[mc->request.refresh.num_coins]; + enum TALER_ErrorCode ec_denomination_sign; + size_t noreveal_idx = mc->request.refresh.noreveal_index; + + memset (csds, + 0, + sizeof(csds)); + + /* Pick the chosen blinded coins */ + for (size_t i = 0; i<mc->request.refresh.num_coins; i++) + { + csds[i].bp = &mc->request.planchets[noreveal_idx][i]; + csds[i].h_denom_pub = &mc->request.denoms_h[i]; + } + + ec_denomination_sign = TEH_keys_denomination_batch_sign ( + mc->request.refresh.num_coins, + csds, + true, /* for melt */ + mc->request.refresh.denom_sigs); + if (TALER_EC_NONE != ec_denomination_sign) + { + GNUNET_break (0); + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_DENOMINATION_SIGN, + ec_denomination_sign); + return; + } + + /* Save the hash of chosen planchets */ + mc->request.refresh.selected_h = + mc->request.kappa_planchets_h.tuple[noreveal_idx]; + + /* If applicable, save the chosen transfer public keys */ + if (! mc->request.refresh.is_v27_refresh) + mc->request.refresh.transfer_pubs = + mc->request.transfer_pubs[noreveal_idx]; + + /** + * For the denominations with cipher CS, calculate the R-values + * and save the choices we made now, as at a later point, the + * private keys for the denominations might now be available anymore + * to make the same choice again. + */ + if (0 < mc->request.refresh.num_cs_r_values) + { + size_t num_cs_r_values = mc->request.refresh.num_cs_r_values; + struct TEH_CsDeriveData cdds[num_cs_r_values]; + struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_values]; + + memset (nonces, + 0, + sizeof(nonces)); + mc->request.refresh.cs_r_values = + GNUNET_new_array (num_cs_r_values, + struct GNUNET_CRYPTO_CSPublicRPairP); + mc->request.refresh.cs_r_choices = 0; + + GNUNET_assert (! mc->request.refresh.no_blinding_seed); + TALER_cs_derive_nonces_from_seed ( + &mc->request.refresh.blinding_seed, + true, /* for melt */ + num_cs_r_values, + mc->request.cs_indices, + nonces); + + for (size_t i = 0; i < num_cs_r_values; i++) + { + size_t idx = mc->request.cs_indices[i]; + + GNUNET_assert (idx < mc->request.refresh.num_coins); + cdds[i].h_denom_pub = &mc->request.denoms_h[idx]; + cdds[i].nonce = &nonces[i]; + } + + /** + * Let the crypto helper generate the R-values and + * make the choices + */ + if (TALER_EC_NONE != + TEH_keys_denomination_cs_batch_r_pub_simple ( + mc->request.refresh.num_cs_r_values, + cdds, + true, /* for melt */ + mc->request.refresh.cs_r_values)) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_CRYPTO_HELPER); + return; + } + + /* Now save the choices for the selected bits */ + GNUNET_assert (num_cs_r_values <= 64); + for (size_t i = 0; i < num_cs_r_values; i++) + { + size_t idx = mc->request.cs_indices[i]; + + struct TALER_BlindedDenominationSignature *sig = + &mc->request.refresh.denom_sigs[idx]; + uint64_t bit = sig->blinded_sig->details.blinded_cs_answer.b; + + mc->request.refresh.cs_r_choices |= bit << i; + GNUNET_static_assert ( + TALER_MAX_COINS <= + sizeof(mc->request.refresh.cs_r_choices) * 8); + } + } + } + mc->phase++; +} + + +/** + * Generates response for the melt request. + * + * @param mc melt operation context + */ +static void +phase_generate_reply_success (struct MeltContext *mc) +{ + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *db_obj; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec_confirmation_sign; + + db_obj = mc->request.is_idempotent + ? &mc->request.refresh_idem + : &mc->request.refresh; + ec_confirmation_sign = + TALER_exchange_online_melt_confirmation_sign ( + &TEH_keys_exchange_sign_, + &db_obj->rc, + db_obj->noreveal_index, + &pub, + &sig); + if (TALER_EC_NONE != ec_confirmation_sign) + { + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_CONFIRMATION_SIGN, + ec_confirmation_sign); + return; + } + + finish_loop (mc, + TALER_MHD_REPLY_JSON_PACK ( + mc->rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("noreveal_index", + db_obj->noreveal_index), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub))); +} + + +/** + * Check if the melt request is replayed and we already have an answer. + * If so, replay the existing answer and return the HTTP response. + * + * @param[in,out] mc parsed request data + * @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 +melt_is_idempotent ( + struct MeltContext *mc) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_refresh ( + TEH_plugin->cls, + &mc->request.refresh.rc, + &mc->request.refresh_idem); + if (0 > qs) + { + /* FIXME: soft error not handled correctly! */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "get_refresh"); + return true; /* Well, kind-of. */ + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return false; + + mc->request.is_idempotent = true; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "request is idempotent\n"); + + /* Generate idempotent reply */ + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_MELT]++; + mc->phase = MELT_PHASE_GENERATE_REPLY_SUCCESS; + mc->error.code = MELT_ERROR_NONE; + return true; +} + + +/** + * Reports an error, potentially with details. + * That is, it puts a error-type specific response into the MHD queue. + * It will do a idempotency check first, if needed for the error type. + * + * @param mc melt context + */ +static void +phase_generate_reply_error ( + struct MeltContext *mc) +{ + GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == mc->phase); + GNUNET_assert (MELT_ERROR_NONE != mc->error.code); + + if (IDEMPOTENCY_CHECK_REQUIRED (mc->error.code) && + melt_is_idempotent (mc)) + { + return; + } + + switch (mc->error.code) + { + case MELT_ERROR_NONE: + break; + case MELT_ERROR_REQUEST_PARAMETER_MALFORMED: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + mc->error.details.request_parameter_malformed)); + return; + case MELT_ERROR_KEYS_MISSING: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + return; + case MELT_ERROR_DB_FETCH_FAILED: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + mc->error.details.db_fetch_context)); + return; + case MELT_ERROR_DB_INVARIANT_FAILURE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); + return; + case MELT_ERROR_DB_PREFLIGHT_FAILURE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "make_coin_known")); + return; + case MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure")); + return; + case MELT_ERROR_COIN_UNKNOWN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, + NULL)); + return; + case MELT_COIN_CONFLICTING_DENOMINATION_KEY: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, + TALER_B2S (&mc->error.details.denom_h))); + return; + case MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, + NULL)); + return; + case MELT_ERROR_DENOMINATION_SIGN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + mc->error.details.ec_denomination_sign, + NULL)); + return; + case MELT_ERROR_DENOMINATION_SIGNATURE_INVALID: + finish_loop (mc, + TALER_MHD_reply_with_error (mc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)); + return; + case MELT_ERROR_DENOMINATION_KEY_UNKNOWN: + GNUNET_break_op (0); + finish_loop (mc, + TEH_RESPONSE_reply_unknown_denom_pub_hash ( + mc->rc->connection, + &mc->error.details.denom_h)); + return; + case MELT_ERROR_DENOMINATION_EXPIRED: + GNUNET_break_op (0); + finish_loop (mc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + mc->rc->connection, + &mc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "MELT")); + return; + case MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + finish_loop (mc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + mc->rc->connection, + &mc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "MELT")); + return; + case MELT_ERROR_DENOMINATION_REVOKED: + GNUNET_break_op (0); + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + NULL)); + return; + case MELT_ERROR_COIN_CIPHER_MISMATCH: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)); + return; + case MELT_ERROR_BLINDING_SEED_REQUIRED: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_GENERIC_PARAMETER_MISSING, + "blinding_seed")); + return; + case MELT_ERROR_CRYPTO_HELPER: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL)); + return; + case MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION: + { + char msg[256]; + + GNUNET_snprintf (msg, + sizeof(msg), + "denomination %s does not support age restriction", + GNUNET_h2s (&mc->error.details.denom_h.hash)); + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + msg)); + return; + } + case MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, + "old_age_commitment_h")); + return; + case MELT_ERROR_AMOUNT_OVERFLOW: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, + "amount")); + return; + case MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, + "amount+fee")); + return; + case MELT_ERROR_FEES_EXCEED_CONTRIBUTION: + finish_loop (mc, + TALER_MHD_reply_with_error (mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, + NULL)); + return; + case MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, + "value_with_fee incorrect")); + return; + case MELT_ERROR_CONFIRMATION_SIGN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + mc->error.details.ec_confirmation_sign, + NULL)); + return; + case MELT_ERROR_INSUFFICIENT_FUNDS: + finish_loop (mc, + TEH_RESPONSE_reply_coin_insufficient_funds ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &mc->request.refresh.coin.denom_pub_hash, + &mc->request.refresh.coin.coin_pub)); + return; + case MELT_ERROR_DUPLICATE_PLANCHET: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error! */ + "duplicate planchet")); + return; + case MELT_ERROR_DUPLICATE_TRANSFER_PUB: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error! */ + "duplicate transfer_pub")); + return; + case MELT_ERROR_NONCE_RESUSE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error */ + "nonce reuse")); + return; + case MELT_ERROR_COIN_SIGNATURE_INVALID: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_MELT_COIN_SIGNATURE_INVALID, + NULL)); + return; + } + GNUNET_break (0); + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "error phase without error")); +} + + +/** + * Function implementing melt 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 MeltContext *` + * @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 +melt_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct MeltContext *mc = cls; + enum GNUNET_DB_QueryStatus qs; + bool balance_ok; + bool found; + bool nonce_reuse; + uint32_t noreveal_index; + struct TALER_Amount insufficient_funds; + + (void) connection; + (void) mhd_ret; + + qs = TEH_plugin->do_refresh (TEH_plugin->cls, + &mc->request.refresh, + &mc->now, + &found, + &noreveal_index, + &mc->zombie_required, + &nonce_reuse, + &balance_ok, + &insufficient_funds); + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "do_refresh"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_UNKNOWN); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (found) + { + /** + * This request is idempotent, set the nonreveal_index + * to the previous one and reply success. + */ + mc->request.refresh.noreveal_index = noreveal_index; + mc->phase = MELT_PHASE_GENERATE_REPLY_SUCCESS; + mc->error.code = MELT_ERROR_NONE; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + if (nonce_reuse) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_NONCE_RESUSE); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + GNUNET_break_op (0); + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_INSUFFICIENT_FUNDS, + insufficient_funds); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (mc->zombie_required) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT]++; + return qs; +} + + +/** + * The request was prepared successfully. + * Run the main DB transaction. + * + * @param mc The context for the current melt request + */ +static void +phase_run_transaction ( + struct MeltContext *mc) +{ + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_DB_PREFLIGHT_FAILURE); + return; + } + + /* first, make sure coin is known */ + if (! mc->coin_is_known) + { + MHD_RESULT mhd_ret = -1; + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++) + { + qs = TEH_make_coin_known (&mc->request.refresh.coin, + mc->rc->connection, + &mc->known_coin_id, + &mhd_ret); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + GNUNET_break (0); + /* Check if an answer has been queued */ + switch (mhd_ret) + { + case MHD_NO: + mc->phase = MELT_PHASE_RETURN_NO; + return; + case MHD_YES: + mc->phase = MELT_PHASE_RETURN_YES; + return; + default: /* ignore */ + ; + } + SET_ERROR (mc, + MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE); + return; + } + } + + /* run main database transaction */ + { + MHD_RESULT mhd_ret = -1; + enum GNUNET_GenericReturnValue ret; + enum MeltPhase current_phase = mc->phase; + + GNUNET_assert (MELT_PHASE_RUN_TRANSACTION == + current_phase); + ret = TEH_DB_run_transaction (mc->rc->connection, + "run melt", + TEH_MT_REQUEST_MELT, + &mhd_ret, + &melt_transaction, + mc); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + /* Check if an answer has been queued */ + switch (mhd_ret) + { + case MHD_NO: + mc->phase = MELT_PHASE_RETURN_NO; + return; + case MHD_YES: + mc->phase = MELT_PHASE_RETURN_YES; + return; + default: /* ignore */ + ; + } + GNUNET_assert (MELT_ERROR_NONE != mc->error.code); + GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == mc->phase); + return; + } + /** + * In case of idempotency (which is not an error condition), + * the phase has changed in melt_transaction. + * We simple return. + */ + if (current_phase != mc->phase) + return; + } + mc->phase++; +} + + +MHD_RESULT +TEH_handler_melt ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]) +{ + struct MeltContext *mc = rc->rh_ctx; + + (void) args; + if (NULL == mc) + { + mc = GNUNET_new (struct MeltContext); + rc->rh_ctx = mc; + rc->rh_cleaner = &clean_melt_rc; + mc->rc = rc; + mc->now = GNUNET_TIME_timestamp_get (); + } + + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "melt processing in phase %d\n", + mc->phase); + switch (mc->phase) + { + case MELT_PHASE_PARSE: + phase_parse_request (mc, + root); + break; + case MELT_PHASE_CHECK_MELT_VALID: + phase_check_melt_valid (mc); + break; + case MELT_PHASE_CHECK_KEYS: + phase_check_keys (mc); + break; + case MELT_PHASE_CHECK_COIN_SIGNATURE: + phase_check_coin_signature (mc); + break; + case MELT_PHASE_PREPARE_TRANSACTION: + phase_prepare_transaction (mc); + break; + case MELT_PHASE_RUN_TRANSACTION: + phase_run_transaction (mc); + break; + case MELT_PHASE_GENERATE_REPLY_SUCCESS: + phase_generate_reply_success (mc); + break; + case MELT_PHASE_GENERATE_REPLY_ERROR: + phase_generate_reply_error (mc); + break; + case MELT_PHASE_RETURN_YES: + return MHD_YES; + case MELT_PHASE_RETURN_NO: + return MHD_NO; + } + } +} diff --git a/src/exchange/taler-exchange-httpd_post-melt.h b/src/exchange/taler-exchange-httpd_post-melt.h @@ -0,0 +1,52 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-melt.h + * @brief Handle /melt requests, starting with vDOLDPLUS of the protocol + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_MELT_H +#define TALER_EXCHANGE_HTTPD_POST_MELT_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_melt_cleanup (void); + + +/** + * Handle a "/melt" request. Parses the request into the JSON + * components and validates the melted coins, the signature and + * execute the melt as database transaction. + * + * @param rc the request context + * @param root uploaded JSON data + * @param args array of additional options, not used + * @return MHD result code + */ +MHD_RESULT +TEH_handler_melt ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c @@ -0,0 +1,664 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-purses-PURSE_PUB-create.c + * @brief Handle /purses/$PID/create requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_post-purses-PURSE_PUB-create.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_exchangedb_lib.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for #create_transaction. + */ +struct PurseCreateContext +{ + + /** + * Total actually deposited by all the coins. + */ + struct TALER_Amount deposit_total; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Merge key for the purse. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Encrypted contract of for the purse. + */ + struct TALER_EncryptedContract econtract; + + /** + * Signature of the client affiming this request. + */ + struct TALER_PurseContractSignatureP purse_sig; + + /** + * Fundamental details about the purse. + */ + struct TEH_PurseDetails pd; + + /** + * Array of coins being deposited. + */ + struct TEH_PurseDepositedCoin *coins; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + + /** + * Minimum age for deposits into this purse. + */ + uint32_t min_age; + + /** + * Do we have an @e econtract? + */ + bool no_econtract; + +}; + + +/** + * Execute database transaction for /purses/$PID/create. 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 PurseCreateContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +create_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct PurseCreateContext *pcc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount purse_fee; + bool in_conflict = true; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &purse_fee)); + /* 1) create purse */ + qs = TEH_plugin->insert_purse_request ( + TEH_plugin->cls, + &pcc->pd.purse_pub, + &pcc->merge_pub, + pcc->pd.purse_expiration, + &pcc->pd.h_contract_terms, + pcc->min_age, + TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, + &purse_fee, + &pcc->pd.target_amount, + &pcc->purse_sig, + &in_conflict); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ( + "Failed to store create 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 create"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + 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, + &pcc->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); + 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_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) deposit all coins */ + for (unsigned int i = 0; i<pcc->num_coins; i++) + { + struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; + bool balance_ok = false; + bool conflict = true; + bool too_late = true; + + qs = TEH_make_coin_known (&coin->cpi, + connection, + &coin->known_coin_id, + mhd_ret); + if (qs < 0) + return qs; + qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls, + &pcc->pd.purse_pub, + &coin->cpi.coin_pub, + &coin->amount, + &coin->coin_sig, + &coin->amount_minus_fee, + &balance_ok, + &too_late, + &conflict); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0 != qs); + TALER_LOG_WARNING ( + "Failed to store purse deposit information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse create deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Coin %s has insufficient balance for purse deposit of amount %s\n", + TALER_B2S (&coin->cpi.coin_pub), + TALER_amount2s (&coin->amount)); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &coin->cpi.denom_pub_hash, + &coin->cpi.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (too_late) + { + *mhd_ret + = TALER_MHD_reply_with_ec ( + connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "too late to deposit on purse creation"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (conflict) + { + struct TALER_Amount amount; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_AgeCommitmentHashP phac; + char *partner_url = NULL; + + TEH_plugin->rollback (TEH_plugin->cls); + qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls, + &pcc->pd.purse_pub, + &coin->cpi.coin_pub, + &amount, + &h_denom_pub, + &phac, + &coin_sig, + &partner_url); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + TALER_LOG_WARNING ( + "Failed to fetch purse deposit information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get purse deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), + GNUNET_JSON_pack_data_auto ("coin_pub", + &coin_pub), + GNUNET_JSON_pack_data_auto ("coin_sig", + &coin_sig), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &h_denom_pub), + GNUNET_JSON_pack_data_auto ("h_age_restrictions", + &phac), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("partner_url", + partner_url)), + TALER_JSON_pack_amount ("amount", + &amount)); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + /* 3) if present, persist contract */ + if (! pcc->no_econtract) + { + in_conflict = true; + qs = TEH_plugin->insert_contract (TEH_plugin->cls, + &pcc->pd.purse_pub, + &pcc->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 create 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, + &pcc->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; +} + + +/** + * Parse a coin and check signature of the coin and the denomination + * signature over the coin. + * + * @param[in,out] connection our HTTP connection + * @param[in,out] pcc request context + * @param[out] coin coin to initialize + * @param jcoin coin to parse + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +static enum GNUNET_GenericReturnValue +parse_coin (struct MHD_Connection *connection, + struct PurseCreateContext *pcc, + struct TEH_PurseDepositedCoin *coin, + const json_t *jcoin) +{ + enum GNUNET_GenericReturnValue iret; + + if (GNUNET_OK != + (iret = TEH_common_purse_deposit_parse_coin (connection, + coin, + jcoin))) + return iret; + if (GNUNET_OK != + (iret = TEH_common_deposit_check_purse_deposit ( + connection, + coin, + &pcc->pd.purse_pub, + pcc->min_age))) + return iret; + if (0 > + TALER_amount_add (&pcc->deposit_total, + &pcc->deposit_total, + &coin->amount_minus_fee)) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "total deposit contribution")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_purses_create ( + struct TEH_RequestContext *rc, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root) +{ + struct MHD_Connection *connection = rc->connection; + struct PurseCreateContext pcc = { + .pd.purse_pub = *purse_pub, + .exchange_timestamp = GNUNET_TIME_timestamp_get () + }; + const json_t *deposits; + json_t *deposit; + unsigned int idx; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("amount", + TEH_currency, + &pcc.pd.target_amount), + GNUNET_JSON_spec_uint32 ("min_age", + &pcc.min_age), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_econtract ("econtract", + &pcc.econtract), + &pcc.no_econtract), + GNUNET_JSON_spec_fixed_auto ("merge_pub", + &pcc.merge_pub), + GNUNET_JSON_spec_fixed_auto ("purse_sig", + &pcc.purse_sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &pcc.pd.h_contract_terms), + GNUNET_JSON_spec_array_const ("deposits", + &deposits), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &pcc.pd.purse_expiration), + GNUNET_JSON_spec_end () + }; + const struct TEH_GlobalFee *gf; + + { + 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 */ + } + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &pcc.deposit_total)); + if (GNUNET_TIME_timestamp_cmp (pcc.pd.purse_expiration, + <, + pcc.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_PURSE_CREATE_EXPIRATION_BEFORE_NOW, + NULL); + } + if (GNUNET_TIME_absolute_is_never (pcc.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_PURSE_CREATE_EXPIRATION_IS_NEVER, + NULL); + } + pcc.num_coins = json_array_size (deposits); + if ( (0 == pcc.num_coins) || + (pcc.num_coins > TALER_MAX_COINS) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "deposits"); + } + { + 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); + } + gf = TEH_keys_global_fee_by_time (keys, + pcc.exchange_timestamp); + } + if (NULL == gf) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create 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); + } + /* parse deposits */ + pcc.coins = GNUNET_new_array (pcc.num_coins, + struct TEH_PurseDepositedCoin); + json_array_foreach (deposits, idx, deposit) + { + enum GNUNET_GenericReturnValue res; + struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; + + res = parse_coin (connection, + &pcc, + coin, + deposit); + if (GNUNET_OK != res) + { + for (unsigned int i = 0; i<idx; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + GNUNET_JSON_parse_free (spec); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + } + + if (0 < TALER_amount_cmp (&gf->fees.purse, + &pcc.deposit_total)) + { + GNUNET_break_op (0); + GNUNET_free (pcc.coins); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE, + NULL); + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + + if (GNUNET_OK != + TALER_wallet_purse_create_verify ( + pcc.pd.purse_expiration, + &pcc.pd.h_contract_terms, + &pcc.merge_pub, + pcc.min_age, + &pcc.pd.target_amount, + &pcc.pd.purse_pub, + &pcc.purse_sig)) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID, + NULL); + } + if ( (! pcc.no_econtract) && + (GNUNET_OK != + TALER_wallet_econtract_upload_verify (pcc.econtract.econtract, + pcc.econtract.econtract_size, + &pcc.econtract.contract_pub, + purse_pub, + &pcc.econtract.econtract_sig)) ) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + 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); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + 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 create", + TEH_MT_REQUEST_PURSE_CREATE, + &mhd_ret, + &create_transaction, + &pcc)) + { + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + GNUNET_JSON_parse_free (spec); + return mhd_ret; + } + } + + /* generate regular response */ + { + MHD_RESULT res; + + res = TEH_RESPONSE_reply_purse_created (connection, + pcc.exchange_timestamp, + &pcc.deposit_total, + &pcc.pd); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + GNUNET_JSON_parse_free (spec); + return res; + } +} + + +/* end of taler-exchange-httpd_purses_create.c */ diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.h b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.h @@ -0,0 +1,47 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-purses-PURSE_PUB-create.h + * @brief Handle /purses/$PID/create requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_PURSES_PURSE_PUB_CREATE_H +#define TALER_EXCHANGE_HTTPD_POST_PURSES_PURSE_PUB_CREATE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/purses/$PURSE_PUB/create" request. Parses the JSON, and, if + * successful, passes the JSON data to #create_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses create" being executed, or rejected. + * + * @param rc connection to handle + * @param purse_pub public key of the purse + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_create ( + struct TEH_RequestContext *rc, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c @@ -0,0 +1,509 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c + * @brief Handle /purses/$PID/deposit requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler/taler_dbevents.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_exchangedb_lib.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for #deposit_transaction. + */ +struct PurseDepositContext +{ + /** + * Public key of the purse we are creating. + */ + const struct TALER_PurseContractPublicKeyP *purse_pub; + + /** + * Total amount to be put into the purse. + */ + struct TALER_Amount amount; + + /** + * Total actually deposited by all the coins. + */ + struct TALER_Amount deposit_total; + + /** + * When should the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * Hash of the contract (needed for signing). + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Array of coins being deposited. + */ + struct TEH_PurseDepositedCoin *coins; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + + /** + * Minimum age for deposits into this purse. + */ + uint32_t min_age; +}; + + +/** + * Send confirmation of purse creation success to client. + * + * @param connection connection to the client + * @param pcc details about the request that succeeded + * @return MHD result code + */ +static MHD_RESULT +reply_deposit_success (struct MHD_Connection *connection, + const struct PurseDepositContext *pcc) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_purse_created_sign ( + &TEH_keys_exchange_sign_, + pcc->exchange_timestamp, + pcc->purse_expiration, + &pcc->amount, + &pcc->deposit_total, + pcc->purse_pub, + &pcc->h_contract_terms, + &pub, + &sig))) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("total_deposited", + &pcc->deposit_total), + TALER_JSON_pack_amount ("purse_value_after_fees", + &pcc->amount), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + pcc->exchange_timestamp), + GNUNET_JSON_pack_timestamp ("purse_expiration", + pcc->purse_expiration), + GNUNET_JSON_pack_data_auto ("h_contract_terms", + &pcc->h_contract_terms), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * Execute database transaction for /purses/$PID/deposit. 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 PurseDepositContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +deposit_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct PurseDepositContext *pcc = cls; + enum GNUNET_DB_QueryStatus qs; + + qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + for (unsigned int i = 0; i<pcc->num_coins; i++) + { + struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; + bool balance_ok = false; + bool conflict = true; + bool too_late = true; + + qs = TEH_make_coin_known (&coin->cpi, + connection, + &coin->known_coin_id, + mhd_ret); + if (qs < 0) + return qs; + qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls, + pcc->purse_pub, + &coin->cpi.coin_pub, + &coin->amount, + &coin->coin_sig, + &coin->amount_minus_fee, + &balance_ok, + &too_late, + &conflict); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0 != qs); + TALER_LOG_WARNING ( + "Failed to store purse deposit information in database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "do purse deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &coin->cpi.denom_pub_hash, + &coin->cpi.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (too_late) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (conflict) + { + struct TALER_Amount amount; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_DenominationHashP h_denom_pub; + struct TALER_AgeCommitmentHashP phac; + char *partner_url = NULL; + + TEH_plugin->rollback (TEH_plugin->cls); + qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls, + pcc->purse_pub, + &coin->cpi.coin_pub, + &amount, + &h_denom_pub, + &phac, + &coin_sig, + &partner_url); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + TALER_LOG_WARNING ( + "Failed to fetch purse deposit information from database\n"); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get purse deposit"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + *mhd_ret + = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), + GNUNET_JSON_pack_data_auto ("coin_pub", + &coin_pub), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &h_denom_pub), + GNUNET_JSON_pack_data_auto ("h_age_commitment", + &phac), + GNUNET_JSON_pack_data_auto ("coin_sig", + &coin_sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("partner_url", + partner_url)), + TALER_JSON_pack_amount ("amount", + &amount)); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + return qs; +} + + +/** + * Parse a coin and check signature of the coin and the denomination + * signature over the coin. + * + * @param[in,out] connection our HTTP connection + * @param[in,out] pcc request context + * @param[out] coin coin to initialize + * @param jcoin coin to parse + * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, + * #GNUNET_SYSERR on failure and no error could be returned + */ +static enum GNUNET_GenericReturnValue +parse_coin (struct MHD_Connection *connection, + struct PurseDepositContext *pcc, + struct TEH_PurseDepositedCoin *coin, + const json_t *jcoin) +{ + enum GNUNET_GenericReturnValue iret; + + if (GNUNET_OK != + (iret = TEH_common_purse_deposit_parse_coin (connection, + coin, + jcoin))) + return iret; + if (GNUNET_OK != + (iret = TEH_common_deposit_check_purse_deposit ( + connection, + coin, + pcc->purse_pub, + pcc->min_age))) + return iret; + if (0 > + TALER_amount_add (&pcc->deposit_total, + &pcc->deposit_total, + &coin->amount_minus_fee)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "total deposit contribution"); + } + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_purses_deposit ( + struct TEH_RequestContext *rc, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root) +{ + struct MHD_Connection *connection = rc->connection; + struct PurseDepositContext pcc = { + .purse_pub = purse_pub, + .exchange_timestamp = GNUNET_TIME_timestamp_get () + }; + const json_t *deposits; + json_t *deposit; + unsigned int idx; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("deposits", + &deposits), + 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 */ + } + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &pcc.deposit_total)); + pcc.num_coins = (unsigned int) json_array_size (deposits); + if ( (0 == pcc.num_coins) || + (((size_t) pcc.num_coins) != json_array_size (deposits)) || + (pcc.num_coins > TALER_MAX_COINS) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "deposits"); + } + + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp create_timestamp; + struct GNUNET_TIME_Timestamp merge_timestamp; + bool was_deleted; + bool was_refunded; + + qs = TEH_plugin->select_purse ( + TEH_plugin->cls, + pcc.purse_pub, + &create_timestamp, + &pcc.purse_expiration, + &pcc.amount, + &pcc.deposit_total, + &pcc.h_contract_terms, + &merge_timestamp, + &was_deleted, + &was_refunded); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* handled below */ + } + if (was_refunded || + was_deleted) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_GONE, + was_deleted + ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED + : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, + GNUNET_TIME_timestamp2s (pcc.purse_expiration)); + } + } + + /* parse deposits */ + pcc.coins = GNUNET_new_array (pcc.num_coins, + struct TEH_PurseDepositedCoin); + json_array_foreach (deposits, idx, deposit) + { + enum GNUNET_GenericReturnValue res; + struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; + + res = parse_coin (connection, + &pcc, + coin, + deposit); + if (GNUNET_OK != res) + { + for (unsigned int i = 0; i<idx; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + } + + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + 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 deposit", + TEH_MT_REQUEST_PURSE_DEPOSIT, + &mhd_ret, + &deposit_transaction, + &pcc)) + { + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return mhd_ret; + } + } + { + struct TALER_PurseEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), + .purse_pub = *pcc.purse_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying about purse deposit %s\n", + TALER_B2S (pcc.purse_pub)); + TEH_plugin->event_notify (TEH_plugin->cls, + &rep.header, + NULL, + 0); + } + + /* generate regular response */ + { + MHD_RESULT res; + + res = reply_deposit_success (connection, + &pcc); + for (unsigned int i = 0; i<pcc.num_coins; i++) + TEH_common_purse_deposit_free_coin (&pcc.coins[i]); + GNUNET_free (pcc.coins); + return res; + } +} + + +/* end of taler-exchange-httpd_purses_deposit.c */ diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h @@ -0,0 +1,47 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h + * @brief Handle /purses/$PID/deposit requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_PURSES_PURSE_PUB_DEPOSIT_H +#define TALER_EXCHANGE_HTTPD_POST_PURSES_PURSE_PUB_DEPOSIT_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/purses/$PURSE_PUB/deposit" request. Parses the JSON, and, if + * successful, passes the JSON data to #deposit_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses deposit" being executed, or rejected. + * + * @param rc request to handle + * @param purse_pub public key of the purse + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_deposit ( + struct TEH_RequestContext *rc, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-merge.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-merge.c @@ -0,0 +1,815 @@ +/* + This file is part of TALER + Copyright (C) 2022-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-purses-PURSE_PUB-merge.c + * @brief Handle /purses/$PID/merge requests; parses the POST and JSON and + * verifies the reserve signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_dbevents.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_post-purses-PURSE_PUB-merge.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_exchangedb_lib.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for #merge_transaction. + */ +struct PurseMergeContext +{ + + /** + * Kept in a DLL. + */ + struct PurseMergeContext *next; + + /** + * Kept in a DLL. + */ + struct PurseMergeContext *prev; + + /** + * Our request. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for the legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Fees that apply to this operation. + */ + const struct TALER_WireFeeSet *wf; + + /** + * Base URL of the exchange provider hosting the reserve. + */ + char *provider_url; + + /** + * URI of the account the purse is to be merged into. + * Must be of the form 'payto://taler-reserve/$EXCHANGE_URL/RESERVE_PUB'. + */ + struct TALER_NormalizedPayto payto_uri; + + /** + * Response to return, if set. + */ + struct MHD_Response *response; + + /** + * Public key of the purse we are creating. + */ + struct TALER_PurseContractPublicKeyP purse_pub; + + /** + * Total amount to be put into the purse. + */ + struct TALER_Amount target_amount; + + /** + * Current amount in the purse. + */ + struct TALER_Amount balance; + + /** + * When should the purse expire. + */ + struct GNUNET_TIME_Timestamp purse_expiration; + + /** + * When the client signed the merge. + */ + struct GNUNET_TIME_Timestamp merge_timestamp; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Merge key for the purse. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Signature of the reservce affiming this request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Signature of the client affiming the merge. + */ + struct TALER_PurseMergeSignatureP merge_sig; + + /** + * Public key of the reserve (account), as extracted from @e payto_uri. + */ + union TALER_AccountPublicKeyP account_pub; + + /** + * Hash of the contract terms of the purse. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Hash of the @e payto_uri. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * KYC status of the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * HTTP status to return with @e response, or 0. + */ + unsigned int http_status; + + /** + * Minimum age for deposits into this purse. + */ + uint32_t min_age; + + /** + * Set to true if this request was suspended. + */ + bool suspended; +}; + + +/** + * Kept in a DLL. + */ +static struct PurseMergeContext *pmc_head; + +/** + * Kept in a DLL. + */ +static struct PurseMergeContext *pmc_tail; + + +void +TEH_purses_merge_cleanup () +{ + struct PurseMergeContext *pmc; + + while (NULL != (pmc = pmc_head)) + { + GNUNET_CONTAINER_DLL_remove (pmc_head, + pmc_tail, + pmc); + MHD_resume_connection (pmc->rc->connection); + } +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +legi_result_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct PurseMergeContext *pmc = cls; + + pmc->lch = NULL; + MHD_resume_connection (pmc->rc->connection); + GNUNET_CONTAINER_DLL_remove (pmc_head, + pmc_tail, + pmc); + TALER_MHD_daemon_trigger (); + if (NULL != lcr->response) + { + pmc->response = lcr->response; + pmc->http_status = lcr->http_status; + return; + } + pmc->kyc = lcr->kyc; +} + + +/** + * Send confirmation of purse creation success to client. + * + * @param pmc details about the request that succeeded + * @return MHD result code + */ +static MHD_RESULT +reply_merge_success (const struct PurseMergeContext *pmc) +{ + struct MHD_Connection *connection = pmc->rc->connection; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + struct TALER_Amount merge_amount; + + if (0 < + TALER_amount_cmp (&pmc->balance, + &pmc->target_amount)) + { + GNUNET_break (0); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_JSON_pack_amount ("balance", + &pmc->balance), + TALER_JSON_pack_amount ("target_amount", + &pmc->target_amount)); + } + if ( (NULL == pmc->provider_url) || + (0 == strcmp (pmc->provider_url, + TEH_base_url)) ) + { + /* wad fee is always zero if we stay at our own exchange */ + merge_amount = pmc->target_amount; + } + else + { +#if WAD_NOT_IMPLEMENTED /* #7271 */ + /* FIXME: figure out partner, lookup wad fee by partner! #7271 */ + if (0 > + TALER_amount_subtract (&merge_amount, + &pmc->target_amount, + &wad_fee)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &merge_amount)); + } +#else + merge_amount = pmc->target_amount; +#endif + } + if (TALER_EC_NONE != + (ec = TALER_exchange_online_purse_merged_sign ( + &TEH_keys_exchange_sign_, + pmc->exchange_timestamp, + pmc->purse_expiration, + &merge_amount, + &pmc->purse_pub, + &pmc->h_contract_terms, + &pmc->account_pub.reserve_pub, + (NULL != pmc->provider_url) + ? pmc->provider_url + : TEH_base_url, + &pub, + &sig))) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("merge_amount", + &merge_amount), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + pmc->exchange_timestamp), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * 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 PurseMergeContext` + * @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 + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +amount_iterator (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct PurseMergeContext *pmc = cls; + enum GNUNET_GenericReturnValue ret; + enum GNUNET_DB_QueryStatus qs; + + ret = cb (cb_cls, + &pmc->target_amount, + GNUNET_TIME_absolute_get ()); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + qs = TEH_plugin->select_merge_amounts_for_kyc_check ( + TEH_plugin->cls, + &pmc->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); + return qs; +} + + +/** + * Execute database transaction for /purses/$PID/merge. 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 PurseMergeContext` + * @param connection MHD request context + * @param[out] mhd_ret set to MHD status on error + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +merge_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct PurseMergeContext *pmc = cls; + enum GNUNET_DB_QueryStatus qs; + bool in_conflict = true; + bool no_balance = true; + bool no_partner = true; + + qs = TEH_plugin->do_purse_merge ( + TEH_plugin->cls, + &pmc->purse_pub, + &pmc->merge_sig, + pmc->merge_timestamp, + &pmc->reserve_sig, + pmc->provider_url, + &pmc->account_pub.reserve_pub, + &no_partner, + &no_balance, + &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, + "purse merge"); + return qs; + } + if (no_partner) + { + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN, + pmc->provider_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (no_balance) + { + *mhd_ret = + TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYMENT_REQUIRED, + TALER_EC_EXCHANGE_PURSE_NOT_FULL, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (in_conflict) + { + struct TALER_PurseMergeSignatureP merge_sig; + struct GNUNET_TIME_Timestamp merge_timestamp; + char *partner_url = NULL; + struct TALER_ReservePublicKeyP reserve_pub; + bool refunded; + + qs = TEH_plugin->select_purse_merge (TEH_plugin->cls, + &pmc->purse_pub, + &merge_sig, + &merge_timestamp, + &partner_url, + &reserve_pub, + &refunded); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + TALER_LOG_WARNING ( + "Failed to fetch merge 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 merge"); + return qs; + } + if (refunded) + { + 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; + } + if (0 != + GNUNET_memcmp (&merge_sig, + &pmc->merge_sig)) + { + *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_CONFLICT, + GNUNET_JSON_pack_timestamp ("merge_timestamp", + merge_timestamp), + GNUNET_JSON_pack_data_auto ("merge_sig", + &merge_sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("partner_url", + partner_url)), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &reserve_pub)); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + /* idempotent! */ + *mhd_ret = reply_merge_success (pmc); + GNUNET_free (partner_url); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + return qs; +} + + +/** + * Purse-merge-specific cleanup routine. Function called + * upon completion of the request that should + * clean up @a rh_ctx. Can be NULL. + * + * @param rc request context to clean up + */ +static void +clean_purse_merge_rc (struct TEH_RequestContext *rc) +{ + struct PurseMergeContext *pmc = rc->rh_ctx; + + if (NULL != pmc->lch) + { + TEH_legitimization_check_cancel (pmc->lch); + pmc->lch = NULL; + } + GNUNET_free (pmc->provider_url); + GNUNET_free (pmc); +} + + +MHD_RESULT +TEH_handler_purses_merge ( + struct TEH_RequestContext *rc, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root) +{ + struct PurseMergeContext *pmc = rc->rh_ctx; + + if (NULL == pmc) + { + pmc = GNUNET_new (struct PurseMergeContext); + rc->rh_ctx = pmc; + rc->rh_cleaner = &clean_purse_merge_rc; + pmc->rc = rc; + pmc->purse_pub = *purse_pub; + pmc->exchange_timestamp + = GNUNET_TIME_timestamp_get (); + + { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_normalized_payto_uri ("payto_uri", + &pmc->payto_uri), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &pmc->reserve_sig), + GNUNET_JSON_spec_fixed_auto ("merge_sig", + &pmc->merge_sig), + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &pmc->merge_timestamp), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->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 */ + } + } + + { + struct TALER_PurseContractSignatureP purse_sig; + enum GNUNET_DB_QueryStatus qs; + + /* Fetch purse details */ + qs = TEH_plugin->get_purse_request ( + TEH_plugin->cls, + &pmc->purse_pub, + &pmc->merge_pub, + &pmc->purse_expiration, + &pmc->h_contract_terms, + &pmc->min_age, + &pmc->target_amount, + &pmc->balance, + &purse_sig); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* continued below */ + break; + } + } + + /* check signatures */ + if (GNUNET_OK != + TALER_wallet_purse_merge_verify ( + pmc->payto_uri, + pmc->merge_timestamp, + &pmc->purse_pub, + &pmc->merge_pub, + &pmc->merge_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE, + NULL); + } + + /* parse 'payto_uri' into pmc->account_pub and provider_url */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received payto: `%s'\n", + pmc->payto_uri.normalized_payto); + if ( (0 != strncmp (pmc->payto_uri.normalized_payto, + "payto://taler-reserve/", + strlen ("payto://taler-reserve/"))) && + (0 != strncmp (pmc->payto_uri.normalized_payto, + "payto://taler-reserve-http/", + strlen ("payto://taler-reserve-http/"))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + + { + bool http; + const char *host; + const char *slash; + + http = (0 == strncmp (pmc->payto_uri.normalized_payto, + "payto://taler-reserve-http/", + strlen ("payto://taler-reserve-http/"))); + host = &pmc->payto_uri.normalized_payto[http + ? strlen ("payto://taler-reserve-http/") + : strlen ("payto://taler-reserve/")]; + slash = strchr (host, + '/'); + if (NULL == slash) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + GNUNET_asprintf (&pmc->provider_url, + "%s://%.*s/", + http ? "http" : "https", + (int) (slash - host), + host); + slash++; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + slash, + strlen (slash), + &pmc->account_pub.reserve_pub, + sizeof (pmc->account_pub.reserve_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + } + TALER_normalized_payto_hash (pmc->payto_uri, + &pmc->h_payto); + if (0 == strcmp (pmc->provider_url, + TEH_base_url)) + { + /* we use NULL to represent 'self' as the provider */ + GNUNET_free (pmc->provider_url); + } + else + { + char *method = GNUNET_strdup ("FIXME-WAD #7271"); + + /* FIXME-#7271: lookup wire method by pmc.provider_url! */ + pmc->wf = TEH_wire_fees_by_time (pmc->exchange_timestamp, + method); + if (NULL == pmc->wf) + { + MHD_RESULT res; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot merge purse: wire fees not configured!\n"); + res = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_WIRE_FEES_MISSING, + method); + GNUNET_free (method); + return res; + } + GNUNET_free (method); + } + + { + struct TALER_Amount zero_purse_fee; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + pmc->target_amount.currency, + &zero_purse_fee)); + if (GNUNET_OK != + TALER_wallet_account_merge_verify ( + pmc->merge_timestamp, + &pmc->purse_pub, + pmc->purse_expiration, + &pmc->h_contract_terms, + &pmc->target_amount, + &zero_purse_fee, + pmc->min_age, + TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, + &pmc->account_pub.reserve_pub, + &pmc->reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE, + NULL); + } + } + { + struct TALER_FullPayto fake_full_payto; + + GNUNET_asprintf (&fake_full_payto.full_payto, + "%s?receiver-name=wallet", + pmc->payto_uri.normalized_payto); + pmc->lch = TEH_legitimization_check ( + &rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, + fake_full_payto, + &pmc->h_payto, + &pmc->account_pub, + &amount_iterator, + pmc, + &legi_result_cb, + pmc); + GNUNET_free (fake_full_payto.full_payto); + } + GNUNET_assert (NULL != pmc->lch); + MHD_suspend_connection (rc->connection); + GNUNET_CONTAINER_DLL_insert (pmc_head, + pmc_tail, + pmc); + return MHD_YES; + } + if (NULL != pmc->response) + { + return MHD_queue_response (rc->connection, + pmc->http_status, + pmc->response); + } + if (! pmc->kyc.ok) + return TEH_RESPONSE_reply_kyc_required ( + rc->connection, + &pmc->h_payto, + &pmc->kyc, + false); + + /* execute merge transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "execute purse merge", + TEH_MT_REQUEST_PURSE_MERGE, + &mhd_ret, + &merge_transaction, + pmc)) + { + return mhd_ret; + } + } + + { + struct TALER_PurseEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_MERGED), + .purse_pub = pmc->purse_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying about purse merge\n"); + TEH_plugin->event_notify (TEH_plugin->cls, + &rep.header, + NULL, + 0); + } + + /* generate regular response */ + return reply_merge_success (pmc); +} + + +/* end of taler-exchange-httpd_purses_merge.c */ diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-merge.h b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-merge.h @@ -0,0 +1,54 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-purses-PURSE_PUB-merge.h + * @brief Handle /purses/$PID/merge requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_PURSES_PURSE_PUB_MERGE_H +#define TALER_EXCHANGE_HTTPD_POST_PURSES_PURSE_PUB_MERGE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_purses_merge_cleanup (void); + + +/** + * Handle a "/purses/$PURSE_PUB/merge" request. Parses the JSON, and, if + * successful, passes the JSON data to #merge_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses merge" being executed, or rejected. + * + * @param rc request to handle + * @param purse_pub public key of the purse + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_purses_merge ( + struct TEH_RequestContext *rc, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-recoup-refresh.c b/src/exchange/taler-exchange-httpd_post-recoup-refresh.c @@ -0,0 +1,431 @@ +/* + This file is part of TALER + Copyright (C) 2017-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-recoup-refresh.c + * @brief Handle /recoup-refresh requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_db.h" +#include "taler-exchange-httpd_post-recoup-refresh.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler/taler_exchangedb_lib.h" + + +/** + * Closure for #recoup_refresh_transaction(). + */ +struct RecoupContext +{ + + /** + * Set by #recoup_transaction() to the old coin that will + * receive the recoup. + */ + struct TALER_CoinSpendPublicKeyP old_coin_pub; + + /** + * Details about the coin. + */ + const struct TALER_CoinPublicInfo *coin; + + /** + * Key used to blind the coin. + */ + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; + + /** + * Signature of the coin requesting recoup. + */ + const struct TALER_CoinSpendSignatureP *coin_sig; + + /** + * Unique ID of the coin in the known_coins table. + */ + uint64_t known_coin_id; + + /** + * Unique ID of the refresh reveal context of the melt for the new coin. + */ + uint64_t rrc_serial; + + /** + * Set by #recoup_transaction to the timestamp when the recoup + * was accepted. + */ + struct GNUNET_TIME_Timestamp now; + +}; + + +/** + * Execute a "recoup-refresh". The validity of the coin and signature have + * already been checked. The database must now check that the coin is not + * (double) spent, and execute the transaction. + * + * 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 the `struct RecoupContext *` + * @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 code + */ +static enum GNUNET_DB_QueryStatus +recoup_refresh_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct RecoupContext *pc = cls; + enum GNUNET_DB_QueryStatus qs; + bool recoup_ok; + bool internal_failure; + + /* Finally, store new refund data */ + pc->now = GNUNET_TIME_timestamp_get (); + qs = TEH_plugin->do_recoup_refresh (TEH_plugin->cls, + &pc->old_coin_pub, + pc->rrc_serial, + pc->coin_bks, + &pc->coin->coin_pub, + pc->known_coin_id, + pc->coin_sig, + &pc->now, + &recoup_ok, + &internal_failure); + 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_recoup_refresh"); + return qs; + } + + if (internal_failure) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "coin transaction history"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! recoup_ok) + { + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &pc->coin->denom_pub_hash, + &pc->coin->coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} + + +/** + * We have parsed the JSON information about the recoup request. Do + * some basic sanity checks (especially that the signature on the + * request and coin is valid) and then execute the recoup operation. + * Note that we need the DB to check the fee structure, so this is not + * done here but during the recoup_transaction(). + * + * @param connection the MHD connection to handle + * @param coin information about the coin + * @param exchange_vals values contributed by the exchange + * during refresh + * @param coin_bks blinding data of the coin (to be checked) + * @param nonce withdraw nonce (if CS is used) + * @param coin_sig signature of the coin + * @return MHD result code + */ +static MHD_RESULT +verify_and_execute_recoup_refresh ( + struct MHD_Connection *connection, + const struct TALER_CoinPublicInfo *coin, + const struct TALER_ExchangeBlindingValues *exchange_vals, + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, + const union GNUNET_CRYPTO_BlindSessionNonce *nonce, + const struct TALER_CoinSpendSignatureP *coin_sig) +{ + struct RecoupContext pc; + const struct TEH_DenominationKey *dk; + MHD_RESULT mret; + struct TALER_BlindedCoinHashP h_blind; + + /* check denomination exists and is in recoup mode */ + dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, + connection, + &mret); + if (NULL == dk) + return mret; + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + { + /* This denomination is past the expiration time for recoup */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "RECOUP-REFRESH"); + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "RECOUP-REFRESH"); + } + if (! dk->recoup_possible) + { + /* This denomination is not eligible for recoup */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE, + "RECOUP-REFRESH"); + } + + /* check denomination signature */ + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } + if (GNUNET_YES != + TALER_test_coin_valid (coin, + &dk->denom_pub)) + { + TALER_LOG_WARNING ("Invalid coin passed for recoup\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL); + } + + /* check recoup request signature */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_recoup_refresh_verify (&coin->denom_pub_hash, + coin_bks, + &coin->coin_pub, + coin_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID, + NULL); + } + + { + struct TALER_CoinPubHashP c_hash; + struct TALER_BlindedPlanchet blinded_planchet; + + if (GNUNET_OK != + TALER_denom_blind (&dk->denom_pub, + coin_bks, + nonce, + &coin->h_age_commitment, + &coin->coin_pub, + exchange_vals, + &c_hash, + &blinded_planchet)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED, + NULL); + } + TALER_coin_ev_hash (&blinded_planchet, + &coin->denom_pub_hash, + &h_blind); + TALER_blinded_planchet_free (&blinded_planchet); + } + + pc.coin_sig = coin_sig; + pc.coin_bks = coin_bks; + pc.coin = coin; + + { + MHD_RESULT mhd_ret = MHD_NO; + enum GNUNET_DB_QueryStatus qs; + + /* make sure coin is 'known' in database */ + qs = TEH_make_coin_known (coin, + connection, + &pc.known_coin_id, + &mhd_ret); + /* no transaction => no serialization failures should be possible */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + if (qs < 0) + return mhd_ret; + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_old_coin_by_h_blind (TEH_plugin->cls, + &h_blind, + &pc.old_coin_pub, + &pc.rrc_serial); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_old_coin_by_h_blind"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Recoup-refresh requested for unknown envelope %s\n", + GNUNET_h2s (&h_blind.hash)); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND, + NULL); + } + } + + /* Perform actual recoup transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "run recoup-refresh", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &recoup_refresh_transaction, + &pc)) + return mhd_ret; + } + /* Recoup succeeded, return result */ + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ( + "old_coin_pub", + &pc.old_coin_pub)); +} + + +/** + * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_recoup_refresh (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root) +{ + enum GNUNET_GenericReturnValue ret; + struct TALER_CoinPublicInfo coin = {0}; + union GNUNET_CRYPTO_BlindingSecretP coin_bks; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_ExchangeBlindingValues exchange_vals; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + bool no_nonce; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &coin.denom_pub_hash), + TALER_JSON_spec_denom_sig ("denom_sig", + &coin.denom_sig), + TALER_JSON_spec_exchange_blinding_values ("ewv", + &exchange_vals), + GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", + &coin_bks), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &coin_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &coin.h_age_commitment), + &coin.no_age_commitment), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("nonce", + &nonce), + &no_nonce), + GNUNET_JSON_spec_end () + }; + + memset (&coin, + 0, + sizeof (coin)); + coin.coin_pub = *coin_pub; + ret = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + { + MHD_RESULT res; + + res = verify_and_execute_recoup_refresh (connection, + &coin, + &exchange_vals, + &coin_bks, + no_nonce + ? NULL + : &nonce, + &coin_sig); + GNUNET_JSON_parse_free (spec); + return res; + } +} + + +/* end of taler-exchange-httpd_recoup-refresh.c */ diff --git a/src/exchange/taler-exchange-httpd_post-recoup-refresh.h b/src/exchange/taler-exchange-httpd_post-recoup-refresh.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2017, 2021 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-recoup-refresh.h + * @brief Handle /recoup-refresh requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_RECOUP_REFRESH_H +#define TALER_EXCHANGE_HTTPD_POST_RECOUP_REFRESH_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_recoup_refresh (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c b/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c @@ -0,0 +1,443 @@ +/* + This file is part of TALER + Copyright (C) 2017-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-recoup-withdraw.c + * @brief Handle /recoup requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_db.h" +#include "taler-exchange-httpd_post-recoup-withdraw.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler/taler_exchangedb_lib.h" + +/** + * Closure for #recoup_transaction. + */ +struct RecoupContext +{ + /** + * Hash identifying the withdraw request. + */ + struct TALER_BlindedCoinHashP h_coin_ev; + + /** + * Set by #recoup_transaction() to the reserve that will + * receive the recoup, if #refreshed is #GNUNET_NO. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Details about the coin. + */ + const struct TALER_CoinPublicInfo *coin; + + /** + * Key used to blind the coin. + */ + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; + + /** + * Signature of the coin requesting recoup. + */ + const struct TALER_CoinSpendSignatureP *coin_sig; + + /** + * Unique ID of the withdraw operation in the withdraw table. + */ + uint64_t withdraw_serial_id; + + /** + * Unique ID of the coin in the known_coins table. + */ + uint64_t known_coin_id; + + /** + * Set by #recoup_transaction to the timestamp when the recoup + * was accepted. + */ + struct GNUNET_TIME_Timestamp now; + +}; + + +/** + * Execute a "recoup". The validity of the coin and signature have + * already been checked. The database must now check that the coin is + * not (double) spent, and execute the transaction. + * + * 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 the `struct RecoupContext *` + * @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 code + */ +static enum GNUNET_DB_QueryStatus +recoup_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct RecoupContext *pc = cls; + enum GNUNET_DB_QueryStatus qs; + bool recoup_ok; + bool internal_failure; + + /* Finally, store new refund data */ + pc->now = GNUNET_TIME_timestamp_get (); + qs = TEH_plugin->do_recoup (TEH_plugin->cls, + &pc->reserve_pub, + pc->withdraw_serial_id, + pc->coin_bks, + &pc->coin->coin_pub, + pc->known_coin_id, + pc->coin_sig, + &pc->now, + &recoup_ok, + &internal_failure); + 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_recoup"); + return qs; + } + + if (internal_failure) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "do_recoup"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! recoup_ok) + { + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &pc->coin->denom_pub_hash, + &pc->coin->coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} + + +/** + * We have parsed the JSON information about the recoup request. Do + * some basic sanity checks (especially that the signature on the + * request and coin is valid) and then execute the recoup operation. + * Note that we need the DB to check the fee structure, so this is not + * done here but during the recoup_transaction(). + * + * @param connection the MHD connection to handle + * @param coin information about the coin + * @param exchange_vals values contributed by the exchange + * during withdrawal + * @param coin_bks blinding data of the coin (to be checked) + * @param h_planchets The hash of the commitment of the original withdraw request + * @param nonce coin's nonce if CS is used + * @param coin_sig signature of the coin + * @return MHD result code + */ +static MHD_RESULT +verify_and_execute_recoup ( + struct MHD_Connection *connection, + const struct TALER_CoinPublicInfo *coin, + const struct TALER_ExchangeBlindingValues *exchange_vals, + const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, + const struct TALER_HashBlindedPlanchetsP *h_planchets, + const union GNUNET_CRYPTO_BlindSessionNonce *nonce, + const struct TALER_CoinSpendSignatureP *coin_sig) +{ + struct RecoupContext pc; + const struct TEH_DenominationKey *dk; + MHD_RESULT mret; + + /* check denomination exists and is in recoup mode */ + dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, + connection, + &mret); + if (NULL == dk) + return mret; + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + { + /* This denomination is past the expiration time for recoup */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "RECOUP"); + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "RECOUP"); + } + if (! dk->recoup_possible) + { + /* This denomination is not eligible for recoup */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_RECOUP_NOT_ELIGIBLE, + "RECOUP"); + } + + /* check denomination signature */ + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } + if (GNUNET_YES != + TALER_test_coin_valid (coin, + &dk->denom_pub)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL); + } + + /* check recoup request signature */ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_recoup_verify (&coin->denom_pub_hash, + coin_bks, + &coin->coin_pub, + coin_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID, + NULL); + } + + /* re-compute client-side blinding so we can + (a bit later) check that this coin was indeed + signed by us. */ + { + struct TALER_CoinPubHashP c_hash; + struct TALER_BlindedPlanchet blinded_planchet; + + if (GNUNET_OK != + TALER_denom_blind (&dk->denom_pub, + coin_bks, + nonce, + &coin->h_age_commitment, + &coin->coin_pub, + exchange_vals, + &c_hash, + &blinded_planchet)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, + NULL); + } + TALER_coin_ev_hash (&blinded_planchet, + &coin->denom_pub_hash, + &pc.h_coin_ev); + TALER_blinded_planchet_free (&blinded_planchet); + } + + pc.coin_sig = coin_sig; + pc.coin_bks = coin_bks; + pc.coin = coin; + + { + MHD_RESULT mhd_ret = MHD_NO; + enum GNUNET_DB_QueryStatus qs; + + /* make sure coin is 'known' in database */ + qs = TEH_make_coin_known (coin, + connection, + &pc.known_coin_id, + &mhd_ret); + /* no transaction => no serialization failures should be possible */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + if (qs < 0) + return mhd_ret; + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_reserve_by_h_planchets ( + TEH_plugin->cls, + h_planchets, + &pc.reserve_pub, + &pc.withdraw_serial_id); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_reserve_by_commitment"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Recoup requested for unknown envelope %s\n", + GNUNET_h2s (&pc.h_coin_ev.hash)); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND, + NULL); + } + } + + /* Perform actual recoup transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "run recoup", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &recoup_transaction, + &pc)) + return mhd_ret; + } + /* Recoup succeeded, return result */ + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ( + "reserve_pub", + &pc.reserve_pub)); +} + + +/** + * Handle a "/coins/$COIN_PUB/recoup" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_recoup() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_recoup (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root) +{ + enum GNUNET_GenericReturnValue ret; + struct TALER_CoinPublicInfo coin; + union GNUNET_CRYPTO_BlindingSecretP coin_bks; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_ExchangeBlindingValues exchange_vals; + struct TALER_HashBlindedPlanchetsP h_planchets; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + bool no_nonce; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &coin.denom_pub_hash), + TALER_JSON_spec_denom_sig ("denom_sig", + &coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("h_planchets", + &h_planchets), + TALER_JSON_spec_exchange_blinding_values ("ewv", + &exchange_vals), + GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", + &coin_bks), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &coin_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &coin.h_age_commitment), + &coin.no_age_commitment), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("nonce", + &nonce), + &no_nonce), + GNUNET_JSON_spec_end () + }; + + memset (&coin, + 0, + sizeof (coin)); + coin.coin_pub = *coin_pub; + ret = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + { + MHD_RESULT res; + + res = verify_and_execute_recoup (connection, + &coin, + &exchange_vals, + &coin_bks, + &h_planchets, + no_nonce + ? NULL + : &nonce, + &coin_sig); + GNUNET_JSON_parse_free (spec); + return res; + } +} + + +/* end of taler-exchange-httpd_recoup.c */ diff --git a/src/exchange/taler-exchange-httpd_post-recoup-withdraw.h b/src/exchange/taler-exchange-httpd_post-recoup-withdraw.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2017 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-recoup-withdraw.h + * @brief Handle /recoup requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_RECOUP_WITHDRAW_H +#define TALER_EXCHANGE_HTTPD_POST_RECOUP_WITHDRAW_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/coins/$COIN_PUB/recoup" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_recoup() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_recoup (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c @@ -0,0 +1,575 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c + * @brief Handle /reserves/$RESERVE_PUB/close requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_post-reserves-RESERVE_PUB-close.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How far do we allow a client's time to be off when + * checking the request timestamp? + */ +#define TIMESTAMP_TOLERANCE \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) + + +/** + * Closure for #reserve_close_transaction. + */ +struct ReserveCloseContext +{ + + /** + * Kept in a DLL. + */ + struct ReserveCloseContext *next; + + /** + * Kept in a DLL. + */ + struct ReserveCloseContext *prev; + + /** + * Our request context. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Where to wire the funds, may be NULL. + */ + struct TALER_FullPayto payto_uri; + + /** + * Response to return. Note that the response must + * be queued or destroyed by the callee. NULL + * if the legitimization check was successful and the handler should return + * a handler-specific result. + */ + struct MHD_Response *response; + + /** + * Public key of the reserve the inquiry is about. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Timestamp of the request. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Client signature approving the request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Amount that will be wired (after closing fees). + */ + struct TALER_Amount wire_amount; + + /** + * Current balance of the reserve. + */ + struct TALER_Amount balance; + + /** + * Hash of the @e payto_uri, if given (otherwise zero). + */ + struct TALER_FullPaytoHashP h_payto; + + /** + * KYC status for the request. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Hash of the payto-URI that was used for the KYC decision. + */ + struct TALER_NormalizedPaytoHashP kyc_payto; + + /** + * Query status from the amount_it() helper function. + */ + enum GNUNET_DB_QueryStatus qs; + + /** + * HTTP status code for @a response, or 0 + */ + unsigned int http_status; + + /** + * Set to true if the request was suspended. + */ + bool suspended; + + /** + * Set to true if the request was suspended. + */ + bool resumed; +}; + + +/** + * Kept in a DLL. + */ +static struct ReserveCloseContext *rcc_head; + +/** + * Kept in a DLL. + */ +static struct ReserveCloseContext *rcc_tail; + + +void +TEH_reserves_close_cleanup () +{ + struct ReserveCloseContext *rcc; + + while (NULL != (rcc = rcc_head)) + { + GNUNET_CONTAINER_DLL_remove (rcc_head, + rcc_tail, + rcc); + MHD_resume_connection (rcc->rc->connection); + } +} + + +/** + * Send reserve close to client. + * + * @param rhc reserve close to return + * @return MHD result code + */ +static MHD_RESULT +reply_reserve_close_success ( + const struct ReserveCloseContext *rhc) +{ + struct MHD_Connection *connection = rhc->rc->connection; + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("wire_amount", + &rhc->wire_amount)); +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +reserve_close_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct ReserveCloseContext *rcc = cls; + + rcc->lch = NULL; + rcc->http_status = lcr->http_status; + rcc->response = lcr->response; + rcc->kyc = lcr->kyc; + GNUNET_CONTAINER_DLL_remove (rcc_head, + rcc_tail, + rcc); + MHD_resume_connection (rcc->rc->connection); + rcc->resumed = true; + rcc->suspended = false; + TALER_MHD_daemon_trigger (); +} + + +/** + * 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 + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +amount_it (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct ReserveCloseContext *rcc = cls; + enum GNUNET_GenericReturnValue ret; + + ret = cb (cb_cls, + &rcc->balance, + GNUNET_TIME_absolute_get ()); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + rcc->qs + = TEH_plugin->iterate_reserve_close_info ( + TEH_plugin->cls, + &rcc->kyc_payto, + limit, + cb, + cb_cls); + return rcc->qs; +} + + +/** + * Function implementing /reserves/$RID/close transaction. Given the public + * key of a reserve, return the associated transaction close. 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 ReserveCloseContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!); unused + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +reserve_close_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveCloseContext *rcc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_FullPayto payto_uri = { + .full_payto = NULL + }; + const struct TALER_WireFeeSet *wf; + + qs = TEH_plugin->select_reserve_close_info ( + TEH_plugin->cls, + &rcc->reserve_pub, + &rcc->balance, + &payto_uri); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_reserve_close_info"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *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; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if ( (NULL == rcc->payto_uri.full_payto) && + (NULL == payto_uri.full_payto) ) + { + *mhd_ret + = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if ( (! rcc->resumed) && + (NULL != rcc->payto_uri.full_payto) && + ( (NULL == payto_uri.full_payto) || + (0 != TALER_full_payto_cmp (payto_uri, + rcc->payto_uri)) ) ) + { + /* KYC check may be needed: we're not returning + the money to the account that funded the reserve + in the first place. */ + + TALER_full_payto_normalize_and_hash (rcc->payto_uri, + &rcc->kyc_payto); + rcc->lch = TEH_legitimization_check ( + &rcc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE, + rcc->payto_uri, + &rcc->kyc_payto, + NULL, /* no account_pub: this is about the origin/destination account */ + &amount_it, + rcc, + &reserve_close_legi_cb, + rcc); + GNUNET_assert (NULL != rcc->lch); + GNUNET_CONTAINER_DLL_insert (rcc_head, + rcc_tail, + rcc); + MHD_suspend_connection (rcc->rc->connection); + rcc->suspended = true; + GNUNET_free (payto_uri.full_payto); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + rcc->kyc.ok = true; + if (NULL == rcc->payto_uri.full_payto) + rcc->payto_uri = payto_uri; + + { + char *method; + + method = TALER_payto_get_method (rcc->payto_uri.full_payto); + wf = TEH_wire_fees_by_time (rcc->timestamp, + method); + if (NULL == wf) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, + method); + GNUNET_free (method); + GNUNET_free (payto_uri.full_payto); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_free (method); + } + + if (0 > + TALER_amount_subtract (&rcc->wire_amount, + &rcc->balance, + &wf->closing)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client attempted to close reserve with insufficient balance.\n"); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &rcc->wire_amount)); + *mhd_ret = reply_reserve_close_success (rcc); + GNUNET_free (payto_uri.full_payto); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->insert_close_request ( + TEH_plugin->cls, + &rcc->reserve_pub, + payto_uri, + &rcc->reserve_sig, + rcc->timestamp, + &rcc->balance, + &wf->closing); + GNUNET_free (payto_uri.full_payto); + rcc->payto_uri.full_payto = NULL; + 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, + "insert_close_request"); + return qs; + } + if (qs <= 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + return qs; +} + + +/** + * Cleanup routine. Function called + * upon completion of the request that should + * clean up @a rh_ctx. Can be NULL. + * + * @param rc request to clean up context for + */ +static void +reserve_close_cleanup (struct TEH_RequestContext *rc) +{ + struct ReserveCloseContext *rcc = rc->rh_ctx; + + if (NULL != rcc->lch) + { + TEH_legitimization_check_cancel (rcc->lch); + rcc->lch = NULL; + } + GNUNET_free (rcc); +} + + +MHD_RESULT +TEH_handler_reserves_close ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct ReserveCloseContext *rcc = rc->rh_ctx; + MHD_RESULT mhd_ret; + + if (NULL == rcc) + { + rcc = GNUNET_new (struct ReserveCloseContext); + rc->rh_ctx = rcc; + rc->rh_cleaner = &reserve_close_cleanup; + rcc->reserve_pub = *reserve_pub; + rcc->rc = rc; + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rcc->timestamp), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_full_payto_uri ("payto_uri", + &rcc->payto_uri), + NULL), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rcc->reserve_sig), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->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 */ + } + } + } + + { + struct GNUNET_TIME_Timestamp now; + + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq ( + now.abs_time, + rcc->timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } + } + + if (NULL != rcc->payto_uri.full_payto) + TALER_full_payto_hash (rcc->payto_uri, + &rcc->h_payto); + if (GNUNET_OK != + TALER_wallet_reserve_close_verify ( + rcc->timestamp, + &rcc->h_payto, + &rcc->reserve_pub, + &rcc->reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE, + NULL); + } + } + if (NULL != rcc->response) + return MHD_queue_response (rc->connection, + rcc->http_status, + rcc->response); + if (rcc->resumed && + (! rcc->kyc.ok) ) + { + if (0 == rcc->kyc.requirement_row) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "requirement row not set"); + } + return TEH_RESPONSE_reply_kyc_required ( + rc->connection, + &rcc->kyc_payto, + &rcc->kyc, + false); + } + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "reserve close", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_close_transaction, + rcc)) + { + return mhd_ret; + } + if (rcc->suspended) + return MHD_YES; + return reply_reserve_close_success (rcc); +} + + +/* end of taler-exchange-httpd_reserves_close.c */ diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.h b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.h @@ -0,0 +1,48 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-RESERVE_PUB-close.h + * @brief Handle /reserves/$RESERVE_PUB/close requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_RESERVES_RESERVE_PUB_CLOSE_H +#define TALER_EXCHANGE_HTTPD_POST_RESERVES_RESERVE_PUB_CLOSE_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_reserves_close_cleanup (void); + +/** + * Handle a POST "/reserves/$RID/close" request. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @param root uploaded body from the client + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_close ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c @@ -0,0 +1,471 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c + * @brief Handle /reserves/$RESERVE_PUB/open requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_mhd_lib.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_dbevents.h" +#include "taler-exchange-httpd_common_deposit.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_post-reserves-RESERVE_PUB-open.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How far do we allow a client's time to be off when + * checking the request timestamp? + */ +#define TIMESTAMP_TOLERANCE \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) + + +/** + * Closure for #reserve_open_transaction. + */ +struct ReserveOpenContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /** + * Desired (minimum) expiration time for the reserve. + */ + struct GNUNET_TIME_Timestamp desired_expiration; + + /** + * Actual expiration time for the reserve. + */ + struct GNUNET_TIME_Timestamp reserve_expiration; + + /** + * Timestamp of the request. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Client signature approving the request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Global fees applying to the request. + */ + const struct TEH_GlobalFee *gf; + + /** + * Amount to be paid from the reserve. + */ + struct TALER_Amount reserve_payment; + + /** + * Actual cost to open the reserve. + */ + struct TALER_Amount open_cost; + + /** + * Total amount that was deposited. + */ + struct TALER_Amount total; + + /** + * Information about payments by coin. + */ + struct TEH_PurseDepositedCoin *payments; + + /** + * Length of the @e payments array. + */ + unsigned int payments_len; + + /** + * Desired minimum purse limit. + */ + uint32_t purse_limit; + + /** + * Set to true if the reserve balance is too low + * for the operation. + */ + bool no_funds; + +}; + + +/** + * Send reserve open to client. + * + * @param connection connection to the client + * @param rsc reserve open data to return + * @return MHD result code + */ +static MHD_RESULT +reply_reserve_open_success (struct MHD_Connection *connection, + const struct ReserveOpenContext *rsc) +{ + struct GNUNET_TIME_Timestamp now; + struct GNUNET_TIME_Timestamp re; + unsigned int status; + + status = MHD_HTTP_OK; + if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, + <, + rsc->desired_expiration)) + status = MHD_HTTP_PAYMENT_REQUIRED; + now = GNUNET_TIME_timestamp_get (); + if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, + <, + now)) + re = now; + else + re = rsc->reserve_expiration; + return TALER_MHD_REPLY_JSON_PACK ( + connection, + status, + GNUNET_JSON_pack_timestamp ("reserve_expiration", + re), + TALER_JSON_pack_amount ("open_cost", + &rsc->open_cost)); +} + + +/** + * Cleans up information in @a rsc, but does not + * free @a rsc itself (allocated on the stack!). + * + * @param[in] rsc struct with information to clean up + */ +static void +cleanup_rsc (struct ReserveOpenContext *rsc) +{ + for (unsigned int i = 0; i<rsc->payments_len; i++) + { + TEH_common_purse_deposit_free_coin (&rsc->payments[i]); + } + GNUNET_free (rsc->payments); +} + + +/** + * Function implementing /reserves/$RID/open transaction. Given the public + * key of a reserve, return the associated transaction open. 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 ReserveOpenContext *` + * @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 +reserve_open_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveOpenContext *rsc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount reserve_balance; + + for (unsigned int i = 0; i<rsc->payments_len; i++) + { + struct TEH_PurseDepositedCoin *coin = &rsc->payments[i]; + bool insufficient_funds = true; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Make coin %u known\n", + i); + qs = TEH_make_coin_known (&coin->cpi, + connection, + &coin->known_coin_id, + mhd_ret); + if (qs < 0) + return qs; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Insert open deposit %u known\n", + i); + qs = TEH_plugin->insert_reserve_open_deposit ( + TEH_plugin->cls, + &coin->cpi, + &coin->coin_sig, + coin->known_coin_id, + &coin->amount, + &rsc->reserve_sig, + rsc->reserve_pub, + &insufficient_funds); + /* 0 == qs is fine, then the coin was already + spent for this very operation as identified + by reserve_sig! */ + 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_reserve_open_deposit"); + return qs; + } + if (insufficient_funds) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handle insufficient funds\n"); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &coin->cpi.denom_pub_hash, + &coin->cpi.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Do reserve open with reserve payment of %s\n", + TALER_amount2s (&rsc->total)); + qs = TEH_plugin->do_reserve_open (TEH_plugin->cls, + /* inputs */ + rsc->reserve_pub, + &rsc->total, + &rsc->reserve_payment, + rsc->purse_limit, + &rsc->reserve_sig, + rsc->desired_expiration, + rsc->timestamp, + &rsc->gf->fees.account, + /* outputs */ + &rsc->no_funds, + &reserve_balance, + &rsc->open_cost, + &rsc->reserve_expiration); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_reserve_open"); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *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; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + if (rsc->no_funds) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TEH_RESPONSE_reply_reserve_insufficient_balance ( + connection, + TALER_EC_EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS, + &reserve_balance, + &rsc->reserve_payment, + rsc->reserve_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_open (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct ReserveOpenContext rsc; + const json_t *payments; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rsc.timestamp), + GNUNET_JSON_spec_timestamp ("reserve_expiration", + &rsc.desired_expiration), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rsc.reserve_sig), + GNUNET_JSON_spec_uint32 ("purse_limit", + &rsc.purse_limit), + GNUNET_JSON_spec_array_const ("payments", + &payments), + TALER_JSON_spec_amount ("reserve_payment", + TEH_currency, + &rsc.reserve_payment), + GNUNET_JSON_spec_end () + }; + + rsc.reserve_pub = reserve_pub; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->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 */ + } + } + + { + struct GNUNET_TIME_Timestamp now; + + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, + rsc.timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } + } + + rsc.payments_len = json_array_size (payments); + rsc.payments = GNUNET_new_array (rsc.payments_len, + struct TEH_PurseDepositedCoin); + rsc.total = rsc.reserve_payment; + for (unsigned int i = 0; i<rsc.payments_len; i++) + { + struct TEH_PurseDepositedCoin *coin = &rsc.payments[i]; + enum GNUNET_GenericReturnValue res; + + res = TEH_common_purse_deposit_parse_coin ( + rc->connection, + coin, + json_array_get (payments, + i)); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + cleanup_rsc (&rsc); + return MHD_YES; /* failure */ + } + if (0 > + TALER_amount_add (&rsc.total, + &rsc.total, + &coin->amount_minus_fee)) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + NULL); + } + } + + { + struct TEH_KeyStateHandle *keys; + + keys = TEH_keys_get_state (); + if (NULL == keys) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + rsc.gf = TEH_keys_global_fee_by_time (keys, + rsc.timestamp); + } + if (NULL == rsc.gf) + { + GNUNET_break (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, + NULL); + } + + if (GNUNET_OK != + TALER_wallet_reserve_open_verify (&rsc.reserve_payment, + rsc.timestamp, + rsc.desired_expiration, + rsc.purse_limit, + reserve_pub, + &rsc.reserve_sig)) + { + GNUNET_break_op (0); + cleanup_rsc (&rsc); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE, + NULL); + } + + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "reserve open", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_open_transaction, + &rsc)) + { + cleanup_rsc (&rsc); + return mhd_ret; + } + } + + { + MHD_RESULT mhd_ret; + + mhd_ret = reply_reserve_open_success (rc->connection, + &rsc); + cleanup_rsc (&rsc); + return mhd_ret; + } +} + + +/* end of taler-exchange-httpd_reserves_open.c */ diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.h b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.h @@ -0,0 +1,41 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-RESERVE_PUB-open.h + * @brief Handle /reserves/$RESERVE_PUB/open requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_RESERVES_RESERVE_PUB_OPEN_H +#define TALER_EXCHANGE_HTTPD_POST_RESERVES_RESERVE_PUB_OPEN_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a POST "/reserves/$RID/open" request. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @param root uploaded body from the client + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_open (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.c b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.c @@ -0,0 +1,894 @@ +/* + This file is part of TALER + Copyright (C) 2022-2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-RESERVE_PUB-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 "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h" +#include "taler-exchange-httpd_responses.h" +#include "taler/taler_exchangedb_lib.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * Closure for #purse_transaction. + */ +struct ReservePurseContext +{ + + /** + * Kept in a DLL. + */ + struct ReservePurseContext *next; + + /** + * Kept in a DLL. + */ + struct ReservePurseContext *prev; + + /** + * Our request context. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Response to return. Note that the response must + * be queued or destroyed by the callee. NULL + * if the legitimization check was successful and the handler should return + * a handler-specific result. + */ + struct MHD_Response *response; + + /** + * Payto URI for the reserve. + */ + struct TALER_NormalizedPayto payto_uri; + + /** + * Public key of the account (reserve) we are creating a purse for. + */ + union TALER_AccountPublicKeyP account_pub; + + /** + * 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_NormalizedPaytoHashP 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; + + /** + * HTTP status code for @a response, or 0 + */ + unsigned int http_status; + + /** + * Do we lack an @e econtract? + */ + bool no_econtract; + + /** + * Set to true if the purse_fee was not given in the REST request. + */ + bool no_purse_fee; + +}; + +/** + * Kept in a DLL. + */ +static struct ReservePurseContext *rpc_head; + +/** + * Kept in a DLL. + */ +static struct ReservePurseContext *rpc_tail; + + +void +TEH_reserves_purse_cleanup () +{ + struct ReservePurseContext *rpc; + + while (NULL != (rpc = rpc_head)) + { + GNUNET_CONTAINER_DLL_remove (rpc_head, + rpc_tail, + rpc); + MHD_resume_connection (rpc->rc->connection); + } +} + + +/** + * 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 + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +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; + enum GNUNET_GenericReturnValue ret; + + ret = cb (cb_cls, + &rpc->pd.target_amount, + GNUNET_TIME_absolute_get ()); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + 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); + return qs; +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +reserve_purse_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct ReservePurseContext *rpc = cls; + + rpc->lch = NULL; + rpc->http_status = lcr->http_status; + rpc->response = lcr->response; + rpc->kyc = lcr->kyc; + GNUNET_CONTAINER_DLL_remove (rpc_head, + rpc_tail, + rpc); + MHD_resume_connection (rpc->rc->connection); + TALER_MHD_daemon_trigger (); +} + + +/** + * 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; + + { + 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->purse_fee, + &rpc->account_pub.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->purse_fee)) ) ) + { + *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)); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + return qs; +} + + +/** + * Function to clean up our rh_ctx in @a rc + * + * @param[in,out] rc context to clean up + */ +static void +rpc_cleaner (struct TEH_RequestContext *rc) +{ + struct ReservePurseContext *rpc = rc->rh_ctx; + + if (NULL != rpc->lch) + { + TEH_legitimization_check_cancel (rpc->lch); + rpc->lch = NULL; + } + GNUNET_free (rpc->econtract.econtract); + GNUNET_free (rpc->payto_uri.normalized_payto); + GNUNET_free (rpc); +} + + +MHD_RESULT +TEH_handler_reserves_purse ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct ReservePurseContext *rpc = rc->rh_ctx; + struct MHD_Connection *connection = rc->connection; + + if (NULL == rpc) + { + rpc = GNUNET_new (struct ReservePurseContext); + rc->rh_ctx = rpc; + rc->rh_cleaner = &rpc_cleaner; + rpc->rc = rc; + rpc->account_pub.reserve_pub = *reserve_pub; + rpc->exchange_timestamp + = GNUNET_TIME_timestamp_get (); + rpc->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), + &rpc->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 */ + } + } + } + + rpc->payto_uri + = TALER_reserve_make_payto (TEH_base_url, + &rpc->account_pub.reserve_pub); + TALER_normalized_payto_hash (rpc->payto_uri, + &rpc->h_payto); + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + + + if (GNUNET_OK != + TALER_wallet_purse_merge_verify (rpc->payto_uri, + rpc->merge_timestamp, + &rpc->pd.purse_pub, + &rpc->merge_pub, + &rpc->merge_sig)) + { + MHD_RESULT ret; + + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID, + rpc->payto_uri.normalized_payto); + return ret; + } + 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); + 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); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER, + NULL); + } + + { + struct TEH_KeyStateHandle *keys; + const struct TEH_GlobalFee *gf; + + keys = TEH_keys_get_state (); + if (NULL == keys) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + gf = TEH_keys_global_fee_by_time (keys, + rpc->exchange_timestamp); + if (NULL == gf) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create purse: global fees not configured!\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING, + NULL); + } + if (rpc->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, + &gf->fees.purse)) + { + /* rpc->purse_fee is below gf.fees.purse! */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW, + TALER_amount2s (&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); + 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->account_pub.reserve_pub, + &rpc->reserve_sig)) + { + GNUNET_break_op (0); + 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"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID, + NULL); + } + { + struct TALER_FullPayto fake_full_payto; + + GNUNET_asprintf (&fake_full_payto.full_payto, + "%s?receiver-name=wallet", + rpc->payto_uri.normalized_payto); + rpc->lch = TEH_legitimization_check ( + &rpc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, + fake_full_payto, + &rpc->h_payto, + &rpc->account_pub, + &amount_iterator, + rpc, + &reserve_purse_legi_cb, + rpc); + GNUNET_free (fake_full_payto.full_payto); + } + GNUNET_assert (NULL != rpc->lch); + MHD_suspend_connection (rc->connection); + GNUNET_CONTAINER_DLL_insert (rpc_head, + rpc_tail, + rpc); + return MHD_YES; + } + if (NULL != rpc->response) + return MHD_queue_response (connection, + rpc->http_status, + rpc->response); + if (! rpc->kyc.ok) + { + if (0 == rpc->kyc.requirement_row) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "requirement row not set"); + } + return TEH_RESPONSE_reply_kyc_required ( + connection, + &rpc->h_payto, + &rpc->kyc, + false); + } + + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "execute reserve purse", + TEH_MT_REQUEST_RESERVE_PURSE, + &mhd_ret, + &purse_transaction, + rpc)) + { + return mhd_ret; + } + } + + /* generate regular response */ + { + MHD_RESULT res; + + res = TEH_RESPONSE_reply_purse_created ( + connection, + rpc->exchange_timestamp, + &rpc->deposit_total, + &rpc->pd); + return res; + } +} + + +/* end of taler-exchange-httpd_reserves_purse.c */ diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h @@ -0,0 +1,53 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h + * @brief Handle /reserves/$RID/purse requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_RESERVES_RESERVE_PUB_PURSE_H +#define TALER_EXCHANGE_HTTPD_POST_RESERVES_RESERVE_PUB_PURSE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_reserves_purse_cleanup (void); + + +/** + * Handle a "/reserves/$RESERVE_PUB/purse" request. Parses the JSON, and, if + * successful, passes the JSON data to #create_transaction() to further check + * the details of the operation specified. If everything checks out, this + * will ultimately lead to the "purses create" being executed, or rejected. + * + * @param rc request context + * @param reserve_pub public key of the reserve + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_purse ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.c b/src/exchange/taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.c @@ -0,0 +1,391 @@ +/* + This file is part of TALER + Copyright (C) 2014-2022, 2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.c + * @brief Handle /reserves/$RESERVE_PUB/attest requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_dbevents.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How far do we allow a client's time to be off when + * checking the request timestamp? + */ +#define TIMESTAMP_TOLERANCE \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) + + +/** + * Closure for #reserve_attest_transaction. + */ +struct ReserveAttestContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Hash of the payto URI of this reserve. + */ + struct TALER_NormalizedPaytoHashP h_payto; + + /** + * Timestamp of the request. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Expiration time for the attestation. + */ + struct GNUNET_TIME_Timestamp etime; + + /** + * List of requested details. + */ + const json_t *details; + + /** + * Client signature approving the request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Attributes we are affirming. JSON object. + */ + json_t *json_attest; + + /** + * Database error codes encountered. + */ + enum GNUNET_DB_QueryStatus qs; + + /** + * Set to true if we did not find the reserve. + */ + bool not_found; + +}; + + +/** + * Send reserve attest to client. + * + * @param connection connection to the client + * @param rhc reserve attest to return + * @return MHD result code + */ +static MHD_RESULT +reply_reserve_attest_success (struct MHD_Connection *connection, + const struct ReserveAttestContext *rhc) +{ + struct TALER_ExchangeSignatureP exchange_sig; + struct TALER_ExchangePublicKeyP exchange_pub; + enum TALER_ErrorCode ec; + struct GNUNET_TIME_Timestamp now; + + if (NULL == rhc->json_attest) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + NULL); + } + now = GNUNET_TIME_timestamp_get (); + ec = TALER_exchange_online_reserve_attest_details_sign ( + &TEH_keys_exchange_sign_, + now, + rhc->etime, + &rhc->reserve_pub, + rhc->json_attest, + &exchange_pub, + &exchange_sig); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &exchange_pub), + GNUNET_JSON_pack_timestamp ("exchange_timestamp", + now), + GNUNET_JSON_pack_timestamp ("expiration_time", + rhc->etime), + GNUNET_JSON_pack_object_steal ("attributes", + rhc->json_attest)); +} + + +/** + * Function called with information about all applicable + * legitimization processes for the given user. Finds the + * available attributes and merges them into our result + * set based on the details requested by the client. + * + * @param cls our `struct ReserveAttestContext *` + * @param h_payto account for which the attribute data is stored + * @param provider_name provider that must be checked + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + */ +static void +kyc_process_cb (void *cls, + const struct TALER_NormalizedPaytoHashP *h_payto, + const char *provider_name, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes) +{ + struct ReserveAttestContext *rsc = cls; + json_t *attrs; + json_t *val; + const char *name; + bool match = false; + + if (GNUNET_TIME_absolute_is_past (expiration_time.abs_time)) + return; + attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key, + enc_attributes, + enc_attributes_size); + if (NULL == attrs) + { + GNUNET_break (0); + return; + } + json_object_foreach (attrs, name, val) + { + bool requested = strcmp (name, + "FORM_ID"); /* we always return the FORM_ID */ + size_t idx; + json_t *str; + + if (NULL != json_object_get (rsc->json_attest, + name)) + continue; /* duplicate */ + json_array_foreach (rsc->details, idx, str) + { + if (0 == strcmp (json_string_value (str), + name)) + { + requested = true; + break; + } + } + if (! requested) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Skipping attribute `%s': not requested\n", + name); + continue; + } + match = true; + GNUNET_assert (0 == + json_object_set (rsc->json_attest, /* NOT set_new! */ + name, + val)); + } + json_decref (attrs); + if (! match) + return; + rsc->etime = GNUNET_TIME_timestamp_min (expiration_time, + rsc->etime); +} + + +/** + * Function implementing /reserves/$RID/attest transaction. Given the public + * key of a reserve, return the associated transaction attest. 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 ReserveAttestContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!); unused + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +reserve_attest_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveAttestContext *rsc = cls; + enum GNUNET_DB_QueryStatus qs; + + rsc->json_attest = json_object (); + GNUNET_assert (NULL != rsc->json_attest); + qs = TEH_plugin->select_kyc_attributes (TEH_plugin->cls, + &rsc->h_payto, + &kyc_process_cb, + rsc); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_kyc_attributes"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + rsc->not_found = true; + return qs; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + rsc->not_found = false; + break; + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_attest (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]) +{ + struct ReserveAttestContext rsc = { + .etime = GNUNET_TIME_UNIT_FOREVER_TS + }; + MHD_RESULT mhd_ret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rsc.timestamp), + GNUNET_JSON_spec_array_const ("details", + &rsc.details), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rsc.reserve_sig), + GNUNET_JSON_spec_end () + }; + struct GNUNET_TIME_Timestamp now; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &rsc.reserve_pub, + sizeof (rsc.reserve_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, + args[0]); + } + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->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 */ + } + } + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, + rsc.timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } + + if (GNUNET_OK != + TALER_wallet_reserve_attest_request_verify (rsc.timestamp, + rsc.details, + &rsc.reserve_pub, + &rsc.reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE, + NULL); + } + + { + struct TALER_NormalizedPayto payto_uri; + + payto_uri = TALER_reserve_make_payto (TEH_base_url, + &rsc.reserve_pub); + TALER_normalized_payto_hash (payto_uri, + &rsc.h_payto); + GNUNET_free (payto_uri.normalized_payto); + } + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "post reserve attest", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_attest_transaction, + &rsc)) + { + return mhd_ret; + } + if (rsc.not_found) + { + json_decref (rsc.json_attest); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + args[0]); + } + return reply_reserve_attest_success (rc->connection, + &rsc); +} + + +/* end of taler-exchange-httpd_reserves_attest.c */ diff --git a/src/exchange/taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.h b/src/exchange/taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.h @@ -0,0 +1,41 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reserves-attest-RESERVE_PUB.h + * @brief Handle /reserves/$RESERVE_PUB/attest requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_RESERVES_ATTEST_RESERVE_PUB_H +#define TALER_EXCHANGE_HTTPD_POST_RESERVES_ATTEST_RESERVE_PUB_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a POST "/reserves-attest/$RID" request. + * + * @param rc request context + * @param root uploaded body from the client + * @param args args[0] has public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reserves_attest (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[1]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-reveal-melt.c b/src/exchange/taler-exchange-httpd_post-reveal-melt.c @@ -0,0 +1,877 @@ +/* + This file is part of TALER + Copyright (C) 2023,2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reveal-melt.c + * @brief Handle /reveal-melt requests + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler-exchange-httpd_get-metrics.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_post-reveal-melt.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + +#define KAPPA_MINUS_1 (TALER_CNC_KAPPA - 1) + + +/** + * State for an /reveal-melt operation. + */ +struct MeltRevealContext +{ + + /** + * Commitment for the melt operation, previously called by the + * client. + */ + struct TALER_RefreshCommitmentP rc; + + /** + * The data from the original melt. Will be retrieved from + * the DB via @a rc. + */ + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh; + + /** + * True, if @e signatures were not provided in the request. + */ + bool no_signatures; + + /** + * @since v27 + * @deprecated after vDOLDPLUS + * + * TALER_CNC_KAPPA-1 disclosed signatures for public refresh nonces. + */ + struct TALER_PrivateRefreshNonceSignatureP signatures[KAPPA_MINUS_1]; + + /** + * True, if @e transfer_secret_seeds were not provided in the request. + */ + bool no_transfer_secret_seeds; + + /** + * @since vDOLDPLUS + * + * The transfer secret seeds for the revealed batches of coins. + */ + struct TALER_PrivateRefreshBatchSeedP transfer_secret_seeds[KAPPA_MINUS_1]; + + /** + * True, if no @e age_commitment was provided + */ + bool no_age_commitment; + + /** + * If @e no_age_commitment is false, the age commitment of + * the old coin. Needed to ensure that the age commitment + * is applied correctly to the fresh coins. + */ + struct TALER_AgeCommitment age_commitment; +}; + + +/** + * Check if the request belongs to an existing refresh request. + * If so, sets the refresh object with the request data. + * Otherwise, it queues an appropriate MHD response. + * + * @param connection The HTTP connection to the client + * @param rc Original commitment value sent with the melt request + * @param[out] refresh Data from the original refresh request + * @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 refresh request has been found, + * #GNUNET_SYSERR if we did not find the request in the DB + */ +static enum GNUNET_GenericReturnValue +find_original_refresh ( + struct MHD_Connection *connection, + const struct TALER_RefreshCommitmentP *rc, + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh, + MHD_RESULT *result) +{ + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int retry = 0; retry < 3; retry++) + { + qs = TEH_plugin->get_refresh (TEH_plugin->cls, + rc, + refresh); + 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_REFRESHES_REVEAL_SESSION_UNKNOWN, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_HARD_ERROR: + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_refresh"); + 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 unsuccessful retries*/ + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_refresh"); + return GNUNET_SYSERR; +} + + +/** + * Verify that the age commitment is sound, that is, if the + * previous /melt provided a hash, ensure we have the corresponding + * age commitment. Or not, if it wasn't provided. + * + * @param connection The MHD connection to handle + * @param actx The context of the operation, only partially built at this time + * @param[out] mhd_ret The result if a reply is queued for MHD + * @return #GNUNET_OK on success, otherwise a reply is queued for MHD and @a mhd_ret is set + */ +static enum GNUNET_GenericReturnValue +compare_age_commitment ( + struct MHD_Connection *connection, + struct MeltRevealContext *actx, + MHD_RESULT *mhd_ret) +{ + if (actx->no_age_commitment != + actx->refresh.coin.no_age_commitment) + { + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, + NULL); + return GNUNET_SYSERR; + } + if (! actx->no_age_commitment) + { + struct TALER_AgeCommitmentHashP ach; + + actx->age_commitment.mask = TEH_age_restriction_config.mask; + TALER_age_commitment_hash ( + &actx->age_commitment, + &ach); + if (0 != GNUNET_memcmp ( + &actx->refresh.coin.h_age_commitment, + &ach)) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, + NULL); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * @brief Derives an planchet from a given input and returns + * blinded planchets detail + * + * @param connection Connection to the client + * @param denom_key The denomination key + * @param secret The secret to a planchet + * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL + * @param nonce The derived nonce needed for CS denomination + * @param old_age_commitment The age commitment of the old coin, might be NULL + * @param[out] detail planchet detail to write 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 enum GNUNET_GenericReturnValue +calculate_blinded_detail ( + struct MHD_Connection *connection, + struct TEH_DenominationKey *denom_key, + const struct TALER_PlanchetMasterSecretP *secret, + const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub, + union GNUNET_CRYPTO_BlindSessionNonce *nonce, + const struct TALER_AgeCommitment *old_age_commitment, + struct TALER_PlanchetDetail *detail, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret; + struct TALER_AgeCommitmentHashP ach; + bool no_age_commitment = (NULL == old_age_commitment); + + /* calculate age commitment hash */ + if (! no_age_commitment) + { + struct TALER_AgeCommitment nac; + + TALER_age_commitment_derive_from_secret (old_age_commitment, + secret, + &nac); + TALER_age_commitment_hash (&nac, + &ach); + TALER_age_commitment_free (&nac); + } + + /* Next: calculate planchet */ + { + struct TALER_CoinPubHashP c_hash; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; + struct GNUNET_CRYPTO_BlindingInputValues bi = { + .cipher = denom_key->denom_pub.bsign_pub_key->cipher + }; + struct TALER_ExchangeBlindingValues blinding_values = { + .blinding_inputs = &bi + }; + + switch (bi.cipher) + { + case GNUNET_CRYPTO_BSA_CS: + GNUNET_assert (NULL != r_pub); + GNUNET_assert (NULL != nonce); + bi.details.cs_values = *r_pub; + break; + case GNUNET_CRYPTO_BSA_RSA: + break; + default: + GNUNET_assert (0); + } + + TALER_planchet_blinding_secret_create (secret, + &blinding_values, + &bks); + TALER_planchet_setup_coin_priv (secret, + &blinding_values, + &coin_priv); + ret = TALER_planchet_prepare (&denom_key->denom_pub, + &blinding_values, + &bks, + nonce, + &coin_priv, + no_age_commitment + ? NULL + : &ach, + &c_hash, + detail); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + *result = TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + GNUNET_JSON_pack_string ( + "details", + "failed to prepare planchet from base key")); + return ret; + } + } + return ret; +} + + +/** + * @brief Checks the validity of the disclosed signatures as follows: + * - Verifies the validity of the disclosed signatures with the old coin's public key + * - Derives the seeds for disclosed fresh coins + * - Derives the fresh coins from the seeds + * - Derives new age commitment + * - Calculates the blinded coin planchet hashes + * - Calculates the refresh commitment from above data + * - Compares the calculated commitment with existing one + * + * The derivation of a fresh coin from the old coin is defined in + * https://docs.taler.net/design-documents/062-pq-refresh.html + * + * The derivation of age-commitment from a coin's age-commitment + * https://docs.taler.net/design-documents/024-age-restriction.html#melt + * + * @param con HTTP-connection to the client + * @param rf Original refresh object from the previous /melt request + * @param old_age_commitment The age commitment of the original coin + * @param signatures The secrets of the disclosed coins, KAPPA_MINUS_1*num_coins many, maybe NULL + * @param rev_batch_seeds The seeds for the transfer secrets of the disclosed coins, KAPPA_MINUS_1 many, maybe NULL + * @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 ( + struct MHD_Connection *con, + const struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *rf, + const struct TALER_AgeCommitment *old_age_commitment, + const struct TALER_PrivateRefreshNonceSignatureP (*signatures)[KAPPA_MINUS_1], + const struct TALER_PrivateRefreshBatchSeedP (*rev_batch_seeds)[KAPPA_MINUS_1], + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret; + struct TEH_KeyStateHandle *keys; + struct TEH_DenominationKey *denom_keys[rf->num_coins]; + struct TALER_DenominationHashP *denoms_h[rf->num_coins]; + struct TALER_Amount total_amount; + struct TALER_Amount total_fee; + bool is_cs[rf->num_coins]; + size_t cs_count = 0; + + GNUNET_assert (rf->noreveal_index < TALER_CNC_KAPPA); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_fee)); + memset (denom_keys, + 0, + sizeof(denom_keys)); + memset (is_cs, + 0, + sizeof(is_cs)); + + /** + * Consistency check: + * If the refresh was for v27, signatures must not be NULL, + * otherwise the revealed batch seeds must not be NULL. + */ + if (rf->is_v27_refresh) + { + if (NULL == signatures) + { + *result = TALER_MHD_reply_with_error (con, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "signatures missing"); + return GNUNET_SYSERR; + } + } + else /* vDOLDPLUS */ + { + if (NULL == rev_batch_seeds) + { + *result = TALER_MHD_reply_with_error (con, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "batch_seeds missing"); + 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 (con, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + /** + * Find the denomination keys from the original request to /melt + * and keep track of those of type CS. + */ + for (size_t i = 0; i < rf->num_coins; i++) + { + denom_keys[i] = + TEH_keys_denomination_by_serial_from_state ( + keys, + rf->denom_serials[i]); + if (NULL == denom_keys[i]) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + /* Accumulate amount and fees */ + GNUNET_assert (0 <= TALER_amount_add (&total_amount, + &total_amount, + &denom_keys[i]->meta.value)); + GNUNET_assert (0 <= TALER_amount_add (&total_fee, + &total_fee, + &denom_keys[i]->meta.fees.refresh)); + + if (GNUNET_CRYPTO_BSA_CS == + denom_keys[i]->denom_pub.bsign_pub_key->cipher) + { + is_cs[i] = true; + cs_count++; + } + + /* Remember the hash of the public key of the denomination for later */ + denoms_h[i] = &denom_keys[i]->h_denom_pub; + } + + /** + * Sanity check: + * The number CS denominations must match those from the /melt request + */ + if (cs_count != rf->num_cs_r_values) + { + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + return GNUNET_SYSERR; + } + /** + * First things first for v27 clients: Verify the signature of the old coin + * over the refresh nonce. This proves the ownership + * for the fresh coin. + */ + if (rf->is_v27_refresh) + { + size_t sig_idx = 0; + struct TALER_KappaPublicRefreshNoncesP kappa_nonces; + + GNUNET_assert (NULL != signatures); + + /** + * We expand the provided refresh_seed from the original call to /melt, + * into kappa many batch seeds, from which we will later use all except the + * noreveal_index one. + */ + TALER_refresh_expand_kappa_nonces_v27 ( + &rf->refresh_seed, + &kappa_nonces); + + for (uint8_t k=0; k < TALER_CNC_KAPPA; k++) + { + if (rf->noreveal_index == k) + continue; + if (GNUNET_OK != + TALER_wallet_refresh_nonce_verify ( + &rf->coin.coin_pub, + &kappa_nonces.tuple[k], + rf->num_coins, + denoms_h, + k, + &(*signatures)[sig_idx++])) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID, + NULL); + return GNUNET_SYSERR; + } + } + } + /** + * In the following scope, we start collecting blinded coin planchet hashes, + * either those persisted from the original request to /melt, or we + * derive and calculate them from the provided signatures, after having + * verified that each of them was signed by the old coin's private key. + * + * After collecting the blinded coin planchet hashes, we can then get + * the commitment for the calculated values and compare the result with + * the commitment from the /melt request. + */ + { + struct TALER_KappaHashBlindedPlanchetsP kappa_planchets_h; + union GNUNET_CRYPTO_BlindSessionNonce b_nonces[GNUNET_NZL (cs_count)]; + size_t cs_idx = 0; /* [0...cs_count) */ + uint8_t sig_idx = 0; /* [0..KAPPA_MINUS_1) */ + /* These two are only necessary for non-v27 clients, but this is the common case. */ + struct TALER_TransferPublicKeyP k_tpbs[TALER_CNC_KAPPA][rf->num_coins]; + struct TALER_KappaTransferPublicKeys kappa_transfer_pubs = { + .num_transfer_pubs = rf->num_coins + }; + /** + * First, derive the blinding nonces for the CS denominations all at once. + */ + if (0 < cs_count) + { + uint32_t cs_indices[cs_count]; + size_t idx = 0; /* [0...cs_count) */ + + for (size_t i = 0; i < rf->num_coins; i++) + if (is_cs[i]) + cs_indices[idx++] = i; + + TALER_cs_derive_only_cs_blind_nonces_from_seed (&rf->blinding_seed, + true, /* for melt */ + cs_count, + cs_indices, + b_nonces); + } + /** + * We handle the kappa batches of rf->num_coins depths first. + */ + for (uint8_t k = 0; k<TALER_CNC_KAPPA; k++) + { + if (k == rf->noreveal_index) + { + /** + * We take the stored value for the hash of selected batch + */ + kappa_planchets_h.tuple[k] = rf->selected_h; + } + else + { + /** + * We have to generate all the planchets' details from + * the disclosed input material and generate the + * hashes of them. + */ + struct TALER_PlanchetMasterSecretP planchet_secrets[rf->num_coins]; + struct TALER_PlanchetDetail details[rf->num_coins]; + + memset (planchet_secrets, + 0, + sizeof(planchet_secrets)); + memset (details, + 0, + sizeof(details)); + + GNUNET_assert (sig_idx < KAPPA_MINUS_1); + + /** + * Expand from the k-th signature all num_coin planchet secrets, + * except for the noreveal_index. + */ + if (rf->is_v27_refresh) + { + TALER_refresh_signature_to_secrets_v27 ( + &(*signatures)[sig_idx++], + rf->num_coins, + planchet_secrets); + } + else /* vDOLDPLUS */ + { + TALER_refresh_expand_batch_seed_to_transfer_data ( + &(*rev_batch_seeds)[sig_idx++], + &rf->coin.coin_pub, + rf->num_coins, + planchet_secrets, + k_tpbs[k]); + + kappa_transfer_pubs.batch[k] = k_tpbs[k]; + } + /** + * Reset the index for the CS denominations. + */ + cs_idx = 0; + + for (size_t coin_idx = 0; coin_idx < rf->num_coins; coin_idx++) + { + struct GNUNET_CRYPTO_CSPublicRPairP *rp; + union GNUNET_CRYPTO_BlindSessionNonce *np; + + if (is_cs[coin_idx]) + { + GNUNET_assert (cs_idx < cs_count); + np = &b_nonces[cs_idx]; + rp = &rf->cs_r_values[cs_idx]; + cs_idx++; + } + else + { + np = NULL; + rp = NULL; + } + ret = calculate_blinded_detail (con, + denom_keys[coin_idx], + &planchet_secrets[coin_idx], + rp, + np, + old_age_commitment, + &details[coin_idx], + result); + if (GNUNET_OK != ret) + return GNUNET_SYSERR; + } + /** + * Now we can generate the hashes for the kappa-th batch of coins + */ + TALER_wallet_blinded_planchet_details_hash ( + rf->num_coins, + details, + &kappa_planchets_h.tuple[k]); + + for (size_t i =0; i<rf->num_coins; i++) + TALER_planchet_detail_free (&details[i]); + } + } + /** + * Finally, calculate the refresh commitment and compare it with the original. + */ + { + struct TALER_RefreshCommitmentP rc; + + if (rf->is_v27_refresh) + { + TALER_refresh_get_commitment_v27 (&rc, + &rf->refresh_seed, + rf->no_blinding_seed + ? NULL + : &rf->blinding_seed, + &kappa_planchets_h, + &rf->coin.coin_pub, + &rf->amount_with_fee); + } + else + { + TALER_refresh_get_commitment (&rc, + &rf->refresh_seed, + rf->no_blinding_seed + ? NULL + : &rf->blinding_seed, + &kappa_transfer_pubs, + &kappa_planchets_h, + &rf->coin.coin_pub, + &rf->amount_with_fee); + } + + if (0 != GNUNET_CRYPTO_hash_cmp ( + &rf->rc.session_hash, + &rc.session_hash)) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_INVALID_RCH, + "rc"); + return GNUNET_SYSERR; + } + } + } + return GNUNET_OK; +} + + +/** + * @brief Commit the successful reveal to the database + * + * @param con HTTP-connection to the client + * @param rc Original refresh commitment from the previous /melt 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 +commit_reveal ( + struct MHD_Connection *con, + const struct TALER_RefreshCommitmentP *rc, + MHD_RESULT *result) +{ + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int retry = 0; retry < 3; retry++) + { + qs = TEH_plugin->mark_refresh_reveal_success ( + TEH_plugin->cls, + rc); + 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 (con, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_HARD_ERROR: + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_DB_STORE_FAILED, + "mark_refresh_reveal_success"); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + break; /* try again */ + default: + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + return GNUNET_SYSERR; + } + } + /* after unsuccessful retries*/ + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_DB_STORE_FAILED, + "mark_refresh_reveal_success"); + return GNUNET_SYSERR; + +} + + +/** + * @brief Send a response for "/reveal-melt" + * + * @param connection The http connection to the client to send the response to + * @param refresh The data from the previous call to /melt with signatures + * @return a MHD result code + */ +static MHD_RESULT +reply_melt_reveal_success ( + struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh) +{ + json_t *list = json_array (); + GNUNET_assert (NULL != list); + + for (unsigned int i = 0; i < refresh->num_coins; i++) + { + json_t *obj = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig (NULL, + &refresh->denom_sigs[i])); + GNUNET_assert (0 == + json_array_append_new (list, + obj)); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + list)); +} + + +MHD_RESULT +TEH_handler_reveal_melt ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]) +{ + MHD_RESULT result = MHD_NO; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct MeltRevealContext actx = {0}; + struct GNUNET_JSON_Specification sig_tuple[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &actx.signatures[0]), + GNUNET_JSON_spec_fixed_auto (NULL, + &actx.signatures[1]), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification seeds_tuple[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &actx.transfer_secret_seeds[0]), + GNUNET_JSON_spec_fixed_auto (NULL, + &actx.transfer_secret_seeds[1]), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("rc", + &actx.rc), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_tuple_of ("signatures", + sig_tuple), + &actx.no_signatures), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_tuple_of ("batch_seeds", + seeds_tuple), + &actx.no_transfer_secret_seeds), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_age_commitment ("age_commitment", + &actx.age_commitment), + &actx.no_age_commitment), + GNUNET_JSON_spec_end () + }; + + /** + * Note that above, we have hard-wired + * the size of TALER_CNC_KAPPA. + * Let's make sure we keep this in sync. + */ + _Static_assert (KAPPA_MINUS_1 == 2, + "TALER_CNC_KAPPA isn't 3!?!?"); + + /* Parse JSON body*/ + ret = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; + } + + (void) args; + + do { + /* Find original commitment */ + if (GNUNET_OK != + find_original_refresh ( + rc->connection, + &actx.rc, + &actx.refresh, + &result)) + break; + + /* Compare age commitment with the hash from the /melt request, if present */ + if (GNUNET_OK != + compare_age_commitment ( + rc->connection, + &actx, + &result)) + break; + + /* verify the commitment */ + if (GNUNET_OK != + verify_commitment ( + rc->connection, + &actx.refresh, + actx.no_age_commitment + ? NULL + : &actx.age_commitment, + actx.no_signatures + ? NULL + : &actx.signatures, + actx.no_transfer_secret_seeds + ? NULL + : &actx.transfer_secret_seeds, + &result)) + break; + + if (GNUNET_OK != + commit_reveal (rc->connection, + &actx.rc, + &result)) + break; + + /* Finally, return the signatures */ + result = reply_melt_reveal_success (rc->connection, + &actx.refresh); + + } while (0); + + GNUNET_JSON_parse_free (spec); + if (NULL != actx.refresh.denom_sigs) + for (unsigned int i = 0; i<actx.refresh.num_coins; i++) + TALER_blinded_denom_sig_free (&actx.refresh.denom_sigs[i]); + GNUNET_free (actx.refresh.denom_sigs); + GNUNET_free (actx.refresh.denom_pub_hashes); + GNUNET_free (actx.refresh.denom_serials); + GNUNET_free (actx.refresh.cs_r_values); + return result; +} + + +/* end of taler-exchange-httpd_reveal_melt.c */ diff --git a/src/exchange/taler-exchange-httpd_post-reveal-melt.h b/src/exchange/taler-exchange-httpd_post-reveal-melt.h @@ -0,0 +1,58 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reveal-melt.h + * @brief Handle /reveal-melt requests + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_REVEAL_MELT_H +#define TALER_EXCHANGE_HTTPD_POST_REVEAL_MELT_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/reveal-melt" request. + * + * The client got a noreveal_index in response to a previous request + * /melt. It now has to reveal all n*(kappa-1) signatures (except for the + * noreveal_index), signed by the old coin private key, over the coin's + * specific nonce, which is derived per coin from the refresh_seed, + * From that signature also all other coin-relevant data (blinding, age + * restriction, nonce) are derived from. + * + * The exchange computes those values, potentially ensures that age restriction + * is correctly applied, calculates the hash of the blinded envelopes, and - + * together with the non-disclosed blinded envelopes - compares the hash of the + * calculated melt commitment against the original. + * + * If all those checks and the used denominations turn out to be correct, the + * exchange signs all blinded envelopes with their appropriate denomination + * keys. + * + * @param rc request context + * @param root uploaded JSON data + * @param args not used + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reveal_melt ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-reveal-withdraw.c b/src/exchange/taler-exchange-httpd_post-reveal-withdraw.c @@ -0,0 +1,637 @@ +/* + This file is part of TALER + Copyright (C) 2023,2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reveal-withdraw.c + * @brief Handle /reveal-withdraw requests + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler-exchange-httpd_get-metrics.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_exchangedb_plugin.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_post-reveal-withdraw.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" + +/** + * State for an /reveal-withdraw operation. + */ +struct WithdrawRevealContext +{ + + /** + * Commitment for the withdraw operation, previously called by the + * client. + */ + struct TALER_HashBlindedPlanchetsP planchets_h; + + /** + * TALER_CNC_KAPPA-1 secrets for disclosed coin batches. + */ + struct TALER_RevealWithdrawMasterSeedsP disclosed_batch_seeds; + + /** + * The data from the original withdraw. Will be retrieved from + * the DB via @a wch. + */ + struct TALER_EXCHANGEDB_Withdraw withdraw; +}; + + +/** + * Parse the json body of an '/reveal-withdraw' request. It extracts + * the denomination hashes, blinded coins and disclosed coins and allocates + * memory for those. + * + * @param connection The MHD connection to handle + * @param j_disclosed_batch_seeds The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from + * @param[out] actx 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_withdraw_reveal_json ( + struct MHD_Connection *connection, + const json_t *j_disclosed_batch_seeds, + struct WithdrawRevealContext *actx, + MHD_RESULT *mhd_ret) +{ + size_t num_entries; + const char *error; + struct GNUNET_JSON_Specification tuple[] = { + GNUNET_JSON_spec_fixed (NULL, + &actx->disclosed_batch_seeds.tuple[0], + sizeof(actx->disclosed_batch_seeds.tuple[0])), + GNUNET_JSON_spec_fixed (NULL, + &actx->disclosed_batch_seeds.tuple[1], + sizeof(actx->disclosed_batch_seeds.tuple[1])), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_tuple_of (NULL, + tuple), + GNUNET_JSON_spec_end () + }; + + /** + * Note that above, in tuple[], we have hard-wired + * the size of TALER_CNC_KAPPA. + * Let's make sure we keep this in sync. + */ + _Static_assert ((TALER_CNC_KAPPA - 1) == 2); + + num_entries = json_array_size (j_disclosed_batch_seeds); /* 0, if not an array */ + if (! json_is_array (j_disclosed_batch_seeds)) + error = "disclosed_batch_seeds must be an array"; + else if (num_entries == 0) + error = "disclosed_batch_seeds must not be empty"; + else if (num_entries != TALER_CNC_KAPPA - 1) + error = + "disclosed_batch_seeds must be an array of size " + TALER_CNC_KAPPA_MINUS_ONE_STR; + else + error = NULL; + + if ( (NULL != error) || + (GNUNET_OK != + GNUNET_JSON_parse (j_disclosed_batch_seeds, + spec, + &error, + NULL)) ) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + error); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * Check if the request belongs to an existing withdraw request. + * If so, sets the withdraw object with the request data. + * Otherwise, it queues an appropriate MHD response. + * + * @param connection The HTTP connection to the client + * @param planchets_h Original commitment value sent with the withdraw request + * @param[out] withdraw Data from the original withdraw request + * @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 withdraw request has been found, + * #GNUNET_SYSERR if we did not find the request in the DB + */ +static enum GNUNET_GenericReturnValue +find_original_withdraw ( + struct MHD_Connection *connection, + const struct TALER_HashBlindedPlanchetsP *planchets_h, + struct TALER_EXCHANGEDB_Withdraw *withdraw, + MHD_RESULT *result) +{ + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int try = 0; try < 3; try++) + { + qs = TEH_plugin->get_withdraw (TEH_plugin->cls, + planchets_h, + withdraw); + 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_WITHDRAW_COMMITMENT_UNKNOWN, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_HARD_ERROR: + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_withdraw"); + 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 unsuccessful retries*/ + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_withdraw"); + return GNUNET_SYSERR; +} + + +/** + * @brief Derives an age-restricted planchet from a given secret and calculates the hash + * + * @param connection Connection to the client + * @param denom_key The denomination key + * @param secret The secret to a planchet + * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL + * @param nonce The derived nonce needed for CS denomination + * @param max_age The maximum age allowed + * @param[out] bch 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 enum GNUNET_GenericReturnValue +calculate_blinded_hash ( + struct MHD_Connection *connection, + struct TEH_DenominationKey *denom_key, + const struct TALER_PlanchetMasterSecretP *secret, + const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub, + union GNUNET_CRYPTO_BlindSessionNonce *nonce, + uint8_t max_age, + struct TALER_BlindedCoinHashP *bch, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret; + struct TALER_AgeCommitmentHashP ach; + + /* calculate age commitment hash */ + { + struct TALER_AgeCommitmentProof acp; + + TALER_age_restriction_from_secret (secret, + &denom_key->denom_pub.age_mask, + max_age, + &acp); + TALER_age_commitment_hash (&acp.commitment, + &ach); + TALER_age_commitment_proof_free (&acp); + } + + /* Next: calculate planchet */ + { + struct TALER_CoinPubHashP c_hash; + struct TALER_PlanchetDetail detail = {0}; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; + struct GNUNET_CRYPTO_BlindingInputValues bi = { + .cipher = denom_key->denom_pub.bsign_pub_key->cipher + }; + struct TALER_ExchangeBlindingValues alg_values = { + .blinding_inputs = &bi + }; + + if (GNUNET_CRYPTO_BSA_CS == bi.cipher) + { + GNUNET_assert (NULL != r_pub); + GNUNET_assert (NULL != nonce); + bi.details.cs_values = *r_pub; + } + TALER_planchet_blinding_secret_create (secret, + &alg_values, + &bks); + TALER_planchet_setup_coin_priv (secret, + &alg_values, + &coin_priv); + ret = TALER_planchet_prepare (&denom_key->denom_pub, + &alg_values, + &bks, + nonce, + &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, + GNUNET_JSON_pack_string ( + "details", + "failed to prepare planchet from base key")); + return ret; + } + + TALER_coin_ev_hash (&detail.blinded_planchet, + &denom_key->h_denom_pub, + bch); + TALER_blinded_planchet_free (&detail.blinded_planchet); + } + + return ret; +} + + +/** + * @brief Checks the validity of the disclosed coins as follows: + * - Derives and calculates the disclosed coins' + * - public keys, + * - nonces (if applicable), + * - age commitments, + * - blindings + * - blinded hashes + * - Computes planchets_h with those calculated and the undisclosed hashes + * - Compares planchets_h with the value from the original commitment + * - Verifies that all public keys in indices larger than the age group + * corresponding to max_age are derived from the constant public key. + * + * The derivation of the blindings, (potential) nonces and age-commitment from + * a coin's private keys is defined in + * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw + * + * @param con HTTP-connection to the client + * @param wd Original withdraw request + * @param disclosed_batch_seeds The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many + * @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 *con, + const struct TALER_EXCHANGEDB_Withdraw *wd, + const struct TALER_RevealWithdrawMasterSeedsP *disclosed_batch_seeds, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct GNUNET_HashContext *hash_context; + struct TEH_KeyStateHandle *keys; + struct TEH_DenominationKey *denom_keys[wd->num_coins]; + struct TALER_Amount total_amount; + struct TALER_Amount total_fee; + struct TALER_AgeMask mask; + struct TALER_PlanchetMasterSecretP secrets[ + TALER_CNC_KAPPA - 1][wd->num_coins]; + bool is_cs[wd->num_coins]; + size_t cs_count = 0; + uint8_t secrets_idx = 0; /* first index into secrets */ + + GNUNET_assert (wd->noreveal_index < TALER_CNC_KAPPA); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_fee)); + + memset (denom_keys, + 0, + sizeof(denom_keys)); + memset (is_cs, + 0, + sizeof(is_cs)); + + /* 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 (con, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + /* Find the denomination keys */ + for (size_t i = 0; i < wd->num_coins; i++) + { + denom_keys[i] = + TEH_keys_denomination_by_serial_from_state ( + keys, + wd->denom_serials[i]); + if (NULL == denom_keys[i]) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + /* Accumulate amount and fees */ + GNUNET_assert (0 <= TALER_amount_add (&total_amount, + &total_amount, + &denom_keys[i]->meta.value)); + GNUNET_assert (0 <= TALER_amount_add (&total_fee, + &total_fee, + &denom_keys[i]->meta.fees.withdraw)); + + if (i == 0) + mask = denom_keys[i]->meta.age_mask; + GNUNET_assert (mask.bits == denom_keys[i]->meta.age_mask.bits); + + if (GNUNET_CRYPTO_BSA_CS == + denom_keys[i]->denom_pub.bsign_pub_key->cipher) + { + is_cs[i] = true; + cs_count++; + } + } + + hash_context = GNUNET_CRYPTO_hash_context_start (); + + { + uint32_t cs_indices[cs_count]; + union GNUNET_CRYPTO_BlindSessionNonce nonces[cs_count]; + size_t cs_idx = 0; /* [0...cs_count) */ + + for (size_t i = 0; i < wd->num_coins; i++) + if (is_cs[i]) + cs_indices[cs_idx++] = i; + + TALER_cs_derive_only_cs_blind_nonces_from_seed (&wd->blinding_seed, + false, /* not for melt */ + cs_count, + cs_indices, + nonces); + + for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++) + { + if (gamma == wd->noreveal_index) + { + /** + * For the disclosed index, all we have to do is to accumulate the hash + * of the selected coins + */ + GNUNET_CRYPTO_hash_context_read ( + hash_context, + &wd->selected_h, + sizeof(wd->selected_h)); + } + else + { + /** + * For the non-disclosed index, we have to generate the planchets in + * this batch and calculate their hash + */ + struct GNUNET_HashContext *batch_ctx; + struct TALER_BlindedCoinHashP batch_h; + cs_idx = 0; + + batch_ctx = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != batch_ctx); + + /* Expand the secrets for a disclosed batch */ + GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1)); + TALER_withdraw_expand_secrets ( + wd->num_coins, + &disclosed_batch_seeds->tuple[secrets_idx], + secrets[secrets_idx]); + + /** + * Now individually create each coin in this batch and calculate + * its hash, and accumulate the hash of the batch with it + */ + for (size_t coin_idx = 0; coin_idx < wd->num_coins; coin_idx++) + { + struct TALER_BlindedCoinHashP bch; + struct GNUNET_CRYPTO_CSPublicRPairP *rp; + union GNUNET_CRYPTO_BlindSessionNonce *np; + + if (is_cs[coin_idx]) + { + GNUNET_assert (cs_idx < cs_count); + np = &nonces[cs_idx]; + rp = &wd->cs_r_values[cs_idx]; + cs_idx++; + } + else + { + np = NULL; + rp = NULL; + } + ret = calculate_blinded_hash (con, + denom_keys[coin_idx], + &secrets[secrets_idx][coin_idx], + rp, + np, + wd->max_age, + &bch, + result); + if (GNUNET_OK != ret) + { + GNUNET_CRYPTO_hash_context_abort (hash_context); + return GNUNET_SYSERR; + } + /** + * Continue the running hash of all coin hashes in the batch + * with the calculated hash-value of the current, disclosed coin + */ + GNUNET_CRYPTO_hash_context_read (batch_ctx, + &bch, + sizeof(bch)); + } + /** + * Finalize the hash of this batch and add it to + * the total hash + */ + GNUNET_CRYPTO_hash_context_finish ( + batch_ctx, + &batch_h.hash); + GNUNET_CRYPTO_hash_context_read ( + hash_context, + &batch_h, + sizeof(batch_h)); + + secrets_idx++; + } + } + } + + /* Finally, compare the calculated hash with the original wd */ + { + struct TALER_HashBlindedPlanchetsP planchets_h; + + GNUNET_CRYPTO_hash_context_finish ( + hash_context, + &planchets_h.hash); + + if (0 != GNUNET_CRYPTO_hash_cmp ( + &wd->planchets_h.hash, + &planchets_h.hash)) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH, + NULL); + return GNUNET_SYSERR; + } + + } + return GNUNET_OK; +} + + +/** + * @brief Send a response for "/reveal-withdraw" + * + * @param connection The http connection to the client to send the response to + * @param commitment The data from the commitment with signatures + * @return a MHD result code + */ +static MHD_RESULT +reply_withdraw_reveal_success ( + struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Withdraw *commitment) +{ + json_t *list = json_array (); + + GNUNET_assert (NULL != list); + for (size_t i = 0; i < commitment->num_coins; i++) + { + json_t *obj = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig (NULL, + &commitment->denom_sigs[i])); + GNUNET_assert (0 == + json_array_append_new (list, + obj)); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + list)); +} + + +MHD_RESULT +TEH_handler_reveal_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]) +{ + MHD_RESULT result = MHD_NO; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct WithdrawRevealContext actx = {0}; + const json_t *j_disclosed_batch_seeds; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("planchets_h", + &actx.planchets_h), + GNUNET_JSON_spec_array_const ("disclosed_batch_seeds", + &j_disclosed_batch_seeds), + GNUNET_JSON_spec_end () + }; + + (void) args; + /* Parse JSON body*/ + ret = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; + } + + do { + /* Extract denominations, blinded and disclosed coins */ + if (GNUNET_OK != + parse_withdraw_reveal_json ( + rc->connection, + j_disclosed_batch_seeds, + &actx, + &result)) + break; + + /* Find original commitment */ + if (GNUNET_OK != + find_original_withdraw ( + rc->connection, + &actx.planchets_h, + &actx.withdraw, + &result)) + break; + + /* Verify the computed planchets_h 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.withdraw, + &actx.disclosed_batch_seeds, + &result)) + break; + + /* Finally, return the signatures */ + result = reply_withdraw_reveal_success (rc->connection, + &actx.withdraw); + + } while (0); + + GNUNET_JSON_parse_free (spec); + if (NULL != actx.withdraw.denom_sigs) + { + for (size_t i = 0; i<actx.withdraw.num_coins; i++) + TALER_blinded_denom_sig_free (&actx.withdraw.denom_sigs[i]); + GNUNET_free (actx.withdraw.denom_sigs); + } + GNUNET_free (actx.withdraw.cs_r_values); + GNUNET_free (actx.withdraw.denom_pub_hashes); + GNUNET_free (actx.withdraw.denom_serials); + return result; +} + + +/* end of taler-exchange-httpd_reveal_withdraw.c */ diff --git a/src/exchange/taler-exchange-httpd_post-reveal-withdraw.h b/src/exchange/taler-exchange-httpd_post-reveal-withdraw.h @@ -0,0 +1,56 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-reveal-withdraw.h + * @brief Handle /reveal-withdraw/$ACH requests + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_REVEAL_WITHDRAW_H +#define TALER_EXCHANGE_HTTPD_POST_REVEAL_WITHDRAW_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/reveal-withdraw" request. + * + * The client got a noreveal_index in response to a previous request + * /withdraw. It now has to reveal all n*(kappa-1) + * coin's private keys (except for the noreveal_index), from which all other + * coin-relevant data (blinding, age restriction, nonce) is derived from. + * + * The exchange computes those values, ensures that the maximum age is + * correctly applied, calculates the hash of the blinded envelopes, and - + * together with the non-disclosed blinded envelopes - compares the hash of + * the calculated withdraw commitment against the original. + * + * If all those checks and the used denominations turn out to be correct, the + * exchange signs all blinded envelopes with their appropriate denomination + * keys. + * + * @param rc request context + * @param root uploaded JSON data + * @param args not used + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reveal_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_post-withdraw.c b/src/exchange/taler-exchange-httpd_post-withdraw.c @@ -0,0 +1,1818 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-withdraw.c + * @brief Code to handle /withdraw requests + * @note This endpoint is active since v26 of the protocol API + * @author Özgür Kesim + */ + +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler-exchange-httpd.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_post-withdraw.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_get-keys.h" +#include "taler/taler_util.h" + +/** + * The different type of errors that might occur, sorted by name. + * Some of them require idempotency checks, which are marked + * in @e idempotency_check_required below. + */ +enum WithdrawError +{ + WITHDRAW_ERROR_NONE, + WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED, + WITHDRAW_ERROR_AMOUNT_OVERFLOW, + WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, + WITHDRAW_ERROR_BLINDING_SEED_REQUIRED, + WITHDRAW_ERROR_CIPHER_MISMATCH, + WITHDRAW_ERROR_CONFIRMATION_SIGN, + WITHDRAW_ERROR_DB_FETCH_FAILED, + WITHDRAW_ERROR_DB_INVARIANT_FAILURE, + WITHDRAW_ERROR_DENOMINATION_EXPIRED, + WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, + WITHDRAW_ERROR_DENOMINATION_REVOKED, + WITHDRAW_ERROR_DENOMINATION_SIGN, + WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + WITHDRAW_ERROR_FEE_OVERFLOW, + WITHDRAW_ERROR_IDEMPOTENT_PLANCHET, + WITHDRAW_ERROR_INSUFFICIENT_FUNDS, + WITHDRAW_ERROR_CRYPTO_HELPER, + WITHDRAW_ERROR_KEYS_MISSING, + WITHDRAW_ERROR_KYC_REQUIRED, + WITHDRAW_ERROR_LEGITIMIZATION_RESULT, + WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE, + WITHDRAW_ERROR_NONCE_REUSE, + WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN, + WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID, + WITHDRAW_ERROR_RESERVE_UNKNOWN, +}; + +/** + * With the bits set in this value will be mark the errors + * that require a check for idempotency before actually + * returning an error. + */ +static const uint64_t idempotency_check_required = + 0 + | (1LLU << WITHDRAW_ERROR_DENOMINATION_EXPIRED) + | (1LLU << WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN) + | (1LLU << WITHDRAW_ERROR_DENOMINATION_REVOKED) + | (1LLU << WITHDRAW_ERROR_INSUFFICIENT_FUNDS) + | (1LLU << WITHDRAW_ERROR_KEYS_MISSING) + | (1LLU << WITHDRAW_ERROR_KYC_REQUIRED); + +#define IDEMPOTENCY_CHECK_REQUIRED(ec) \ + (0LLU != (idempotency_check_required & (1LLU << (ec)))) + + +/** + * Context for a /withdraw requests + */ +struct WithdrawContext +{ + + /** + * This struct is kept in a DLL. + */ + struct WithdrawContext *prev; + struct WithdrawContext *next; + + /** + * Processing phase we are in. + * The ordering here partially matters, as we progress through + * them by incrementing the phase in the happy path. + */ + enum + { + WITHDRAW_PHASE_PARSE = 0, + WITHDRAW_PHASE_CHECK_KEYS, + WITHDRAW_PHASE_CHECK_RESERVE_SIGNATURE, + WITHDRAW_PHASE_RUN_LEGI_CHECK, + WITHDRAW_PHASE_SUSPENDED, + WITHDRAW_PHASE_CHECK_KYC_RESULT, + WITHDRAW_PHASE_PREPARE_TRANSACTION, + WITHDRAW_PHASE_RUN_TRANSACTION, + WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS, + WITHDRAW_PHASE_GENERATE_REPLY_ERROR, + WITHDRAW_PHASE_RETURN_NO, + WITHDRAW_PHASE_RETURN_YES, + } phase; + + + /** + * Handle for the legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Request context + */ + const struct TEH_RequestContext *rc; + + /** + * KYC status for the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Current time for the DB transaction. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Set to the hash of the normalized payto URI that established + * the reserve. + */ + struct TALER_NormalizedPaytoHashP h_normalized_payto; + + /** + * Captures all parameters provided in the JSON request + */ + struct + { + /** + * All fields (from the request or computed) + * that we persist in the database. + */ + struct TALER_EXCHANGEDB_Withdraw withdraw; + + /** + * In some error cases we check for idempotency. + * If we find an entry in the database, we mark this here. + */ + bool is_idempotent; + + /** + * In some error conditions the request is checked + * for idempotency and the result from the database + * is stored here. + */ + struct TALER_EXCHANGEDB_Withdraw withdraw_idem; + + /** + * Array of ``withdraw.num_coins`` hashes of the public keys + * of the denominations to withdraw. + */ + struct TALER_DenominationHashP *denoms_h; + + /** + * Number of planchets. If ``withdraw.max_age`` was _not_ set, this is equal to ``num_coins``. + * Otherwise (``withdraw.max_age`` was set) it is ``withdraw.num_coins * kappa``. + */ + size_t num_planchets; + + /** + * Array of ``withdraw.num_planchets`` coin planchets. + * Note that the size depends on the age restriction: + * If ``withdraw.age_proof_required`` is false, + * this is an array of length ``withdraw.num_coins``. + * Otherwise it is an array of length ``kappa*withdraw.num_coins``, + * arranged in runs of ``num_coins`` coins, + * [0..num_coins)..[0..num_coins), + * one for each #TALER_CNC_KAPPA value. + */ + struct TALER_BlindedPlanchet *planchets; + + /** + * If proof of age-restriction is required, the #TALER_CNC_KAPPA hashes + * of the batches of ``withdraw.num_coins`` coins. + */ + struct TALER_HashBlindedPlanchetsP kappa_planchets_h[TALER_CNC_KAPPA]; + + /** + * Total (over all coins) amount (excluding fee) committed to withdraw + */ + struct TALER_Amount amount; + + /** + * Total fees for the withdraw + */ + struct TALER_Amount fee; + + /** + * Array of length ``withdraw.num_cs_r_values`` of indices into + * @e denoms_h of CS denominations. + */ + uint32_t *cs_indices; + + } request; + + + /** + * Errors occurring during evaluation of the request are captured in this + * struct. In phase WITHDRAW_PHASE_GENERATE_REPLY_ERROR an appropriate error + * message is prepared and sent to the client. + */ + struct + { + /* The (internal) error code */ + enum WithdrawError code; + + /** + * Some errors require details to be sent to the client. + * These are captured in this union. + * Each field is named according to the error that is using it, except + * commented otherwise. + */ + union + { + const char *request_parameter_malformed; + + const char *reserve_cipher_unknown; + + /** + * For all errors related to a particular denomination, i.e. + * WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, + * WITHDRAW_ERROR_DENOMINATION_EXPIRED, + * WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + * WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + * we use this one field. + */ + const struct TALER_DenominationHashP *denom_h; + + const char *db_fetch_context; + + struct + { + uint16_t max_allowed; + uint32_t birthday; + } maximum_age_too_large; + + /** + * The lowest age required + */ + uint16_t age_restriction_required; + + /** + * Balance of the reserve + */ + struct TALER_Amount insufficient_funds; + + enum TALER_ErrorCode ec_confirmation_sign; + + enum TALER_ErrorCode ec_denomination_sign; + + struct + { + struct MHD_Response *response; + unsigned int http_status; + } legitimization_result; + + } details; + } error; +}; + +/** + * The following macros set the given error code, + * set the phase to WITHDRAW_PHASE_GENERATE_REPLY_ERROR, + * and optionally set the given field (with an optionally given value). + */ +#define SET_ERROR(wc, ec) \ + do \ + { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \ + (wc)->error.code = (ec); \ + (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_FIELD(wc, ec, field) \ + do \ + { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \ + (wc)->error.code = (ec); \ + (wc)->error.details.field = (field); \ + (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_DETAIL(wc, ec, field, value) \ + do \ + { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \ + (wc)->error.code = (ec); \ + (wc)->error.details.field = (value); \ + (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0) + + +/** + * All withdraw context is kept in a DLL. + */ +static struct WithdrawContext *wc_head; +static struct WithdrawContext *wc_tail; + + +void +TEH_withdraw_cleanup () +{ + struct WithdrawContext *wc; + + while (NULL != (wc = wc_head)) + { + GNUNET_CONTAINER_DLL_remove (wc_head, + wc_tail, + wc); + wc->phase = WITHDRAW_PHASE_RETURN_NO; + MHD_resume_connection (wc->rc->connection); + } +} + + +/** + * Terminate the main loop by returning the final + * result. + * + * @param[in,out] wc context to update phase for + * @param mres MHD status to return + */ +static void +finish_loop (struct WithdrawContext *wc, + MHD_RESULT mres) +{ + wc->phase = (MHD_YES == mres) + ? WITHDRAW_PHASE_RETURN_YES + : WITHDRAW_PHASE_RETURN_NO; +} + + +/** + * Check if the withdraw request is replayed + * and we already have an answer. + * If so, replay the existing answer and return the HTTP response. + * + * @param[in,out] wc parsed request data + * @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 +withdraw_is_idempotent ( + struct WithdrawContext *wc) +{ + enum GNUNET_DB_QueryStatus qs; + uint8_t max_retries = 3; + + /* We should at most be called once */ + GNUNET_assert (! wc->request.is_idempotent); + while (0 < max_retries--) + { + qs = TEH_plugin->get_withdraw ( + TEH_plugin->cls, + &wc->request.withdraw.planchets_h, + &wc->request.withdraw_idem); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "get_withdraw"); + return true; /* Well, kind-of. */ + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return false; + + wc->request.is_idempotent = true; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "request is idempotent\n"); + + /* Generate idempotent reply */ + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW]++; + wc->phase = WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS; + return true; +} + + +/** + * Function implementing 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 WithdrawContext *` + * @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 +withdraw_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct WithdrawContext *wc = cls; + enum GNUNET_DB_QueryStatus qs; + bool balance_ok; + bool age_ok; + bool found; + uint16_t noreveal_index; + bool nonce_reuse; + uint16_t allowed_maximum_age; + uint32_t reserve_birthday; + struct TALER_Amount insufficient_funds; + + qs = TEH_plugin->do_withdraw (TEH_plugin->cls, + &wc->request.withdraw, + &wc->now, + &balance_ok, + &insufficient_funds, + &age_ok, + &allowed_maximum_age, + &reserve_birthday, + &found, + &noreveal_index, + &nonce_reuse); + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "do_withdraw"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + SET_ERROR (wc, + WITHDRAW_ERROR_RESERVE_UNKNOWN); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (found) + { + /** + * The request was idempotent and we got the previous noreveal_index. + * We simply overwrite that value in our current withdraw object and + * move on to reply success. + */ + wc->request.withdraw.noreveal_index = noreveal_index; + wc->phase = WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + + if (! age_ok) + { + if (wc->request.withdraw.age_proof_required) + { + wc->error.details.maximum_age_too_large.max_allowed = allowed_maximum_age; + wc->error.details.maximum_age_too_large.birthday = reserve_birthday; + SET_ERROR (wc, + WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE); + } + else + { + wc->error.details.age_restriction_required = allowed_maximum_age; + SET_ERROR (wc, + WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED); + } + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (! balance_ok) + { + TEH_plugin->rollback (TEH_plugin->cls); + SET_ERROR_WITH_FIELD (wc, + WITHDRAW_ERROR_INSUFFICIENT_FUNDS, + insufficient_funds); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (nonce_reuse) + { + GNUNET_break (0); + SET_ERROR (wc, + WITHDRAW_ERROR_NONCE_REUSE); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++; + return qs; +} + + +/** + * The request was prepared successfully. + * Run the main DB transaction. + * + * @param wc The context for the current withdraw request + */ +static void +phase_run_transaction ( + struct WithdrawContext *wc) +{ + MHD_RESULT mhd_ret; + enum GNUNET_GenericReturnValue qs; + + GNUNET_assert (WITHDRAW_PHASE_RUN_TRANSACTION == + wc->phase); + qs = TEH_DB_run_transaction (wc->rc->connection, + "run withdraw", + TEH_MT_REQUEST_WITHDRAW, + &mhd_ret, + &withdraw_transaction, + wc); + if (WITHDRAW_PHASE_RUN_TRANSACTION != wc->phase) + return; + GNUNET_break (GNUNET_OK == qs); + /* If the transaction has changed the phase, we don't alter it and return.*/ + wc->phase++; +} + + +/** + * The request for withdraw was parsed successfully. + * Sign and persist the chosen blinded coins for the reveal step. + * + * @param wc The context for the current withdraw request + */ +static void +phase_prepare_transaction ( + struct WithdrawContext *wc) +{ + size_t offset = 0; + + wc->request.withdraw.denom_sigs + = GNUNET_new_array ( + wc->request.withdraw.num_coins, + struct TALER_BlindedDenominationSignature); + /* Pick the challenge in case of age restriction */ + if (wc->request.withdraw.age_proof_required) + { + wc->request.withdraw.noreveal_index = + GNUNET_CRYPTO_random_u32 ( + GNUNET_CRYPTO_QUALITY_STRONG, + TALER_CNC_KAPPA); + /** + * In case of age restriction, we use the corresponding offset in the planchet + * array to the beginning of the coins corresponding to the noreveal_index. + */ + offset = wc->request.withdraw.noreveal_index + * wc->request.withdraw.num_coins; + GNUNET_assert (offset + wc->request.withdraw.num_coins <= + wc->request.num_planchets); + } + + /* Choose and sign the coins */ + { + struct TEH_CoinSignData csds[wc->request.withdraw.num_coins]; + enum TALER_ErrorCode ec_denomination_sign; + + memset (csds, + 0, + sizeof(csds)); + + /* Pick the chosen blinded coins */ + for (uint32_t i = 0; i<wc->request.withdraw.num_coins; i++) + { + csds[i].bp = &wc->request.planchets[i + offset]; + csds[i].h_denom_pub = &wc->request.denoms_h[i]; + } + + ec_denomination_sign = TEH_keys_denomination_batch_sign ( + wc->request.withdraw.num_coins, + csds, + false, + wc->request.withdraw.denom_sigs); + if (TALER_EC_NONE != ec_denomination_sign) + { + GNUNET_break (0); + SET_ERROR_WITH_FIELD (wc, + WITHDRAW_ERROR_DENOMINATION_SIGN, + ec_denomination_sign); + return; + } + + /* Save the hash value of the selected batch of coins */ + wc->request.withdraw.selected_h = + wc->request.kappa_planchets_h[wc->request.withdraw.noreveal_index]; + } + + /** + * For the denominations with cipher CS, calculate the R-values + * and save the choices we made now, as at a later point, the + * private keys for the denominations might now be available anymore + * to make the same choice again. + */ + if (0 < wc->request.withdraw.num_cs_r_values) + { + size_t num_cs_r_values = wc->request.withdraw.num_cs_r_values; + struct TEH_CsDeriveData cdds[num_cs_r_values]; + struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_values]; + + memset (nonces, + 0, + sizeof(nonces)); + wc->request.withdraw.cs_r_values + = GNUNET_new_array ( + num_cs_r_values, + struct GNUNET_CRYPTO_CSPublicRPairP); + wc->request.withdraw.cs_r_choices = 0; + + GNUNET_assert (! wc->request.withdraw.no_blinding_seed); + TALER_cs_derive_nonces_from_seed ( + &wc->request.withdraw.blinding_seed, + false, /* not for melt */ + num_cs_r_values, + wc->request.cs_indices, + nonces); + + for (size_t i = 0; i < num_cs_r_values; i++) + { + size_t idx = wc->request.cs_indices[i]; + + GNUNET_assert (idx < wc->request.withdraw.num_coins); + cdds[i].h_denom_pub = &wc->request.denoms_h[idx]; + cdds[i].nonce = &nonces[i]; + } + + /** + * Let the crypto helper generate the R-values and make the choices. + */ + if (TALER_EC_NONE != + TEH_keys_denomination_cs_batch_r_pub_simple ( + wc->request.withdraw.num_cs_r_values, + cdds, + false, + wc->request.withdraw.cs_r_values)) + { + GNUNET_break (0); + SET_ERROR (wc, + WITHDRAW_ERROR_CRYPTO_HELPER); + return; + } + + /* Now save the choices for the selected bits */ + for (size_t i = 0; i < num_cs_r_values; i++) + { + size_t idx = wc->request.cs_indices[i]; + + struct TALER_BlindedDenominationSignature *sig = + &wc->request.withdraw.denom_sigs[idx]; + uint8_t bit = sig->blinded_sig->details.blinded_cs_answer.b; + + wc->request.withdraw.cs_r_choices |= bit << i; + GNUNET_static_assert ( + TALER_MAX_COINS <= + sizeof(wc->request.withdraw.cs_r_choices) * 8); + } + } + wc->phase++; +} + + +/** + * Check the KYC result. + * + * @param wc context for request processing + */ +static void +phase_check_kyc_result (struct WithdrawContext *wc) +{ + /* return final positive response */ + if (! wc->kyc.ok) + { + SET_ERROR (wc, + WITHDRAW_ERROR_KYC_REQUIRED); + return; + } + wc->phase++; +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +withdraw_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct WithdrawContext *wc = cls; + + wc->lch = NULL; + GNUNET_assert (WITHDRAW_PHASE_SUSPENDED == + wc->phase); + MHD_resume_connection (wc->rc->connection); + GNUNET_CONTAINER_DLL_remove (wc_head, + wc_tail, + wc); + TALER_MHD_daemon_trigger (); + if (NULL != lcr->response) + { + wc->error.details.legitimization_result.response = lcr->response; + wc->error.details.legitimization_result.http_status = lcr->http_status; + SET_ERROR (wc, + WITHDRAW_ERROR_LEGITIMIZATION_RESULT); + return; + } + wc->kyc = lcr->kyc; + wc->phase = WITHDRAW_PHASE_CHECK_KYC_RESULT; +} + + +/** + * 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 WithdrawContext + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +withdraw_amount_cb ( + void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct WithdrawContext *wc = cls; + enum GNUNET_GenericReturnValue ret; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signaling amount %s for KYC check during witdrawal\n", + TALER_amount2s (&wc->request.withdraw.amount_with_fee)); + + ret = cb (cb_cls, + &wc->request.withdraw.amount_with_fee, + wc->now.abs_time); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( + TEH_plugin->cls, + &wc->h_normalized_payto, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this withdrawal and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); + return qs; +} + + +/** + * Do legitimization check. + * + * @param wc operation context + */ +static void +phase_run_legi_check (struct WithdrawContext *wc) +{ + enum GNUNET_DB_QueryStatus qs; + struct TALER_FullPayto payto_uri; + struct TALER_FullPaytoHashP h_full_payto; + + /* Check if the money came from a wire transfer */ + qs = TEH_plugin->reserves_get_origin ( + TEH_plugin->cls, + &wc->request.withdraw.reserve_pub, + &h_full_payto, + &payto_uri); + if (qs < 0) + { + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "reserves_get_origin"); + return; + } + /* 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_NO_RESULTS == qs) + { + wc->phase = WITHDRAW_PHASE_PREPARE_TRANSACTION; + return; + } + TALER_full_payto_normalize_and_hash (payto_uri, + &wc->h_normalized_payto); + wc->lch = TEH_legitimization_check ( + &wc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, + payto_uri, + &wc->h_normalized_payto, + NULL, /* no account pub: this is about the origin account */ + &withdraw_amount_cb, + wc, + &withdraw_legi_cb, + wc); + GNUNET_assert (NULL != wc->lch); + GNUNET_free (payto_uri.full_payto); + GNUNET_CONTAINER_DLL_insert (wc_head, + wc_tail, + wc); + MHD_suspend_connection (wc->rc->connection); + wc->phase = WITHDRAW_PHASE_SUSPENDED; +} + + +/** + * Check if the given denomination is still or already valid, has not been + * revoked and potentically supports age restriction. + * + * @param[in,out] wc context for the withdraw operation + * @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 denomination key found, might be NULL + * @return true when denomation was found and valid, + * false when denomination was not valid and the state machine was advanced + */ +static enum GNUNET_GenericReturnValue +find_denomination ( + struct WithdrawContext *wc, + struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *denom_h, + struct TEH_DenominationKey **pdk) +{ + struct TEH_DenominationKey *dk; + + *pdk = NULL; + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + denom_h, + NULL, + NULL); + if (NULL == dk) + { + SET_ERROR_WITH_FIELD (wc, + WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, + denom_h); + return false; + } + if (GNUNET_TIME_absolute_is_past ( + dk->meta.expire_withdraw.abs_time)) + { + SET_ERROR_WITH_FIELD (wc, + WITHDRAW_ERROR_DENOMINATION_EXPIRED, + denom_h); + return false; + } + if (GNUNET_TIME_absolute_is_future ( + dk->meta.start.abs_time)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_FIELD (wc, + WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + denom_h); + return false; + } + if (dk->recoup_possible) + { + SET_ERROR (wc, + WITHDRAW_ERROR_DENOMINATION_REVOKED); + return false; + } + /* In case of age withdraw, make sure that the denomination supports age restriction */ + if (wc->request.withdraw.age_proof_required) + { + if (0 == dk->denom_pub.age_mask.bits) + { + GNUNET_break_op (0); + SET_ERROR_WITH_FIELD (wc, + WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + denom_h); + return false; + } + } + *pdk = dk; + return true; +} + + +/** + * Check if the given array of hashes of denomination_keys + * a) belong to valid denominations + * b) those are marked as age restricted, if the request is age restricted + * c) calculate the total amount of the denominations including fees + * for withdraw. + * + * @param wc context of the age withdrawal to check keys for + */ +static void +phase_check_keys ( + struct WithdrawContext *wc) +{ + struct TEH_KeyStateHandle *ksh; + bool is_cs_denom[wc->request.withdraw.num_coins]; + + memset (is_cs_denom, + 0, + sizeof(is_cs_denom)); + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + SET_ERROR (wc, + WITHDRAW_ERROR_KEYS_MISSING); + return; + } + wc->request.withdraw.denom_serials = + GNUNET_new_array (wc->request.withdraw.num_coins, + uint64_t); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &wc->request.amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &wc->request.fee)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &wc->request.withdraw.amount_with_fee)); + + for (unsigned int i = 0; i < wc->request.withdraw.num_coins; i++) + { + struct TEH_DenominationKey *dk; + + if (! find_denomination (wc, + ksh, + &wc->request.denoms_h[i], + &dk)) + return; + switch (dk->denom_pub.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + /* This should never happen (memory corruption?) */ + GNUNET_assert (0); + case GNUNET_CRYPTO_BSA_RSA: + /* nothing to do here */ + break; + case GNUNET_CRYPTO_BSA_CS: + if (wc->request.withdraw.no_blinding_seed) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_BLINDING_SEED_REQUIRED); + return; + } + wc->request.withdraw.num_cs_r_values++; + is_cs_denom[i] = true; + break; + } + + /* Ensure the ciphers from the planchets match the denominations'. */ + if (wc->request.withdraw.age_proof_required) + { + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + size_t off = k * wc->request.withdraw.num_coins; + + if (dk->denom_pub.bsign_pub_key->cipher != + wc->request.planchets[i + off].blinded_message->cipher) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_CIPHER_MISMATCH); + return; + } + } + } + else + { + if (dk->denom_pub.bsign_pub_key->cipher != + wc->request.planchets[i].blinded_message->cipher) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_CIPHER_MISMATCH); + return; + } + } + + /* Accumulate the values */ + if (0 > TALER_amount_add (&wc->request.amount, + &wc->request.amount, + &dk->meta.value)) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_AMOUNT_OVERFLOW); + return; + } + + /* Accumulate the withdraw fees */ + if (0 > TALER_amount_add (&wc->request.fee, + &wc->request.fee, + &dk->meta.fees.withdraw)) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_FEE_OVERFLOW); + return; + } + wc->request.withdraw.denom_serials[i] = dk->meta.serial; + } + + /* Save the hash of the batch of planchets */ + if (! wc->request.withdraw.age_proof_required) + { + TALER_wallet_blinded_planchets_hash ( + wc->request.withdraw.num_coins, + wc->request.planchets, + wc->request.denoms_h, + &wc->request.withdraw.planchets_h); + } + else + { + struct GNUNET_HashContext *ctx; + + /** + * The age-proof-required case is a bit more involved, + * because we need to calculate and remember kappa hashes + * for each batch of coins. + */ + ctx = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != ctx); + + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + size_t off = k * wc->request.withdraw.num_coins; + + TALER_wallet_blinded_planchets_hash ( + wc->request.withdraw.num_coins, + &wc->request.planchets[off], + wc->request.denoms_h, + &wc->request.kappa_planchets_h[k]); + GNUNET_CRYPTO_hash_context_read ( + ctx, + &wc->request.kappa_planchets_h[k], + sizeof(wc->request.kappa_planchets_h[k])); + } + GNUNET_CRYPTO_hash_context_finish ( + ctx, + &wc->request.withdraw.planchets_h.hash); + } + + /* Save the total amount including fees */ + if (0 > TALER_amount_add ( + &wc->request.withdraw.amount_with_fee, + &wc->request.amount, + &wc->request.fee)) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); + return; + } + + /* Save the indices of CS denominations */ + if (0 < wc->request.withdraw.num_cs_r_values) + { + size_t j = 0; + + wc->request.cs_indices = GNUNET_new_array ( + wc->request.withdraw.num_cs_r_values, + uint32_t); + + for (size_t i = 0; i < wc->request.withdraw.num_coins; i++) + { + if (is_cs_denom[i]) + wc->request.cs_indices[j++] = i; + } + } + + wc->phase++; +} + + +/** + * Check that the client signature authorizing the withdrawal is valid. + * + * @param[in,out] wc request context to check + */ +static void +phase_check_reserve_signature ( + struct WithdrawContext *wc) +{ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_withdraw_verify ( + &wc->request.amount, + &wc->request.fee, + &wc->request.withdraw.planchets_h, + wc->request.withdraw.no_blinding_seed + ? NULL + : &wc->request.withdraw.blinding_seed, + (wc->request.withdraw.age_proof_required) + ? &TEH_age_restriction_config.mask + : NULL, + (wc->request.withdraw.age_proof_required) + ? wc->request.withdraw.max_age + : 0, + &wc->request.withdraw.reserve_pub, + &wc->request.withdraw.reserve_sig)) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID); + return; + } + wc->phase++; +} + + +/** + * Free data inside of @a wd, but not @a wd itself. + * + * @param[in] wd withdraw data to free + */ +static void +free_db_withdraw_data (struct TALER_EXCHANGEDB_Withdraw *wd) +{ + if (NULL != wd->denom_sigs) + { + for (unsigned int i = 0; i<wd->num_coins; i++) + TALER_blinded_denom_sig_free (&wd->denom_sigs[i]); + GNUNET_free (wd->denom_sigs); + } + GNUNET_free (wd->denom_serials); + GNUNET_free (wd->cs_r_values); +} + + +/** + * Cleanup routine for withdraw request. + * The function is called upon completion of the request + * that should clean up @a rh_ctx. Can be NULL. + * + * @param rc request context to clean up + */ +static void +clean_withdraw_rc (struct TEH_RequestContext *rc) +{ + struct WithdrawContext *wc = rc->rh_ctx; + + if (NULL != wc->lch) + { + TEH_legitimization_check_cancel (wc->lch); + wc->lch = NULL; + } + GNUNET_free (wc->request.denoms_h); + for (unsigned int i = 0; i<wc->request.num_planchets; i++) + TALER_blinded_planchet_free (&wc->request.planchets[i]); + GNUNET_free (wc->request.planchets); + free_db_withdraw_data (&wc->request.withdraw); + GNUNET_free (wc->request.cs_indices); + if (wc->request.is_idempotent) + free_db_withdraw_data (&wc->request.withdraw_idem); + if ( (WITHDRAW_ERROR_LEGITIMIZATION_RESULT == wc->error.code) && + (NULL != wc->error.details.legitimization_result.response) ) + { + MHD_destroy_response (wc->error.details.legitimization_result.response); + wc->error.details.legitimization_result.response = NULL; + } + GNUNET_free (wc); +} + + +/** + * Generates response for the withdraw request. + * + * @param wc withdraw operation context + */ +static void +phase_generate_reply_success (struct WithdrawContext *wc) +{ + struct TALER_EXCHANGEDB_Withdraw *db_obj; + + db_obj = wc->request.is_idempotent + ? &wc->request.withdraw_idem + : &wc->request.withdraw; + + if (wc->request.withdraw.age_proof_required) + { + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec_confirmation_sign; + + ec_confirmation_sign = + TALER_exchange_online_withdraw_age_confirmation_sign ( + &TEH_keys_exchange_sign_, + &db_obj->planchets_h, + db_obj->noreveal_index, + &pub, + &sig); + if (TALER_EC_NONE != ec_confirmation_sign) + { + SET_ERROR_WITH_FIELD (wc, + WITHDRAW_ERROR_CONFIRMATION_SIGN, + ec_confirmation_sign); + return; + } + + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + wc->rc->connection, + MHD_HTTP_CREATED, + GNUNET_JSON_pack_uint64 ("noreveal_index", + db_obj->noreveal_index), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub))); + } + else /* not age restricted */ + { + json_t *sigs; + + sigs = json_array (); + GNUNET_assert (NULL != sigs); + for (unsigned int i = 0; i<db_obj->num_coins; i++) + { + GNUNET_assert ( + 0 == + json_array_append_new ( + sigs, + GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig ( + NULL, + &db_obj->denom_sigs[i])))); + } + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + wc->rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + sigs))); + } + + TEH_METRICS_withdraw_num_coins += db_obj->num_coins; +} + + +/** + * Reports an error, potentially with details. + * That is, it puts a error-type specific response into the MHD queue. + * It will do a idempotency check first, if needed for the error type. + * + * @param wc withdraw context + */ +static void +phase_generate_reply_error ( + struct WithdrawContext *wc) +{ + GNUNET_assert (WITHDRAW_PHASE_GENERATE_REPLY_ERROR == wc->phase); + if (IDEMPOTENCY_CHECK_REQUIRED (wc->error.code) && + withdraw_is_idempotent (wc)) + { + return; + } + + switch (wc->error.code) + { + case WITHDRAW_ERROR_NONE: + break; + case WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + wc->error.details.request_parameter_malformed)); + return; + case WITHDRAW_ERROR_KEYS_MISSING: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + return; + case WITHDRAW_ERROR_DB_FETCH_FAILED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + wc->error.details.db_fetch_context)); + return; + case WITHDRAW_ERROR_DB_INVARIANT_FAILURE: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); + return; + case WITHDRAW_ERROR_RESERVE_UNKNOWN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL)); + return; + case WITHDRAW_ERROR_DENOMINATION_SIGN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + wc->error.details.ec_denomination_sign, + NULL)); + return; + case WITHDRAW_ERROR_KYC_REQUIRED: + finish_loop (wc, + TEH_RESPONSE_reply_kyc_required ( + wc->rc->connection, + &wc->h_normalized_payto, + &wc->kyc, + false)); + return; + case WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN: + GNUNET_break_op (0); + finish_loop (wc, + TEH_RESPONSE_reply_unknown_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h)); + return; + case WITHDRAW_ERROR_DENOMINATION_EXPIRED: + GNUNET_break_op (0); + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "WITHDRAW")); + return; + case WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "WITHDRAW")); + return; + case WITHDRAW_ERROR_DENOMINATION_REVOKED: + GNUNET_break_op (0); + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + NULL)); + return; + case WITHDRAW_ERROR_CIPHER_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)); + return; + case WITHDRAW_ERROR_BLINDING_SEED_REQUIRED: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_GENERIC_PARAMETER_MISSING, + "blinding_seed")); + return; + case WITHDRAW_ERROR_CRYPTO_HELPER: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL)); + return; + case WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + "cipher")); + return; + case WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION: + { + char msg[256]; + + GNUNET_snprintf (msg, + sizeof(msg), + "denomination %s does not support age restriction", + GNUNET_h2s (&wc->error.details.denom_h->hash)); + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + msg)); + return; + } + case WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Generating JSON response with code %d\n", + (int) TALER_EC_EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE); + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_MHD_PACK_EC ( + TALER_EC_EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE), + GNUNET_JSON_pack_uint64 ( + "allowed_maximum_age", + wc->error.details.maximum_age_too_large.max_allowed), + GNUNET_JSON_pack_uint64 ( + "reserve_birthday", + wc->error.details.maximum_age_too_large.birthday))); + return; + case WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED: + finish_loop (wc, + TEH_RESPONSE_reply_reserve_age_restriction_required ( + wc->rc->connection, + wc->error.details.age_restriction_required)); + return; + case WITHDRAW_ERROR_AMOUNT_OVERFLOW: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW, + "amount")); + return; + case WITHDRAW_ERROR_FEE_OVERFLOW: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW, + "fee")); + return; + case WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + "amount+fee")); + return; + case WITHDRAW_ERROR_CONFIRMATION_SIGN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + wc->error.details.ec_confirmation_sign, + NULL)); + return; + case WITHDRAW_ERROR_INSUFFICIENT_FUNDS: + finish_loop (wc, + TEH_RESPONSE_reply_reserve_insufficient_balance ( + wc->rc->connection, + TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, + &wc->error.details.insufficient_funds, + &wc->request.withdraw.amount_with_fee, + &wc->request.withdraw.reserve_pub)); + return; + case WITHDRAW_ERROR_IDEMPOTENT_PLANCHET: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET, + NULL)); + return; + case WITHDRAW_ERROR_NONCE_REUSE: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, + NULL)); + return; + case WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, + NULL)); + return; + case WITHDRAW_ERROR_LEGITIMIZATION_RESULT: { + finish_loop ( + wc, + MHD_queue_response (wc->rc->connection, + wc->error.details.legitimization_result.http_status, + wc->error.details.legitimization_result.response)); + return; + } + } + GNUNET_break (0); + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "error phase without error")); +} + + +/** + * Initializes the new context for the incoming withdraw request + * + * @param[in,out] wc withdraw request context + * @param root json body of the request + */ +static void +withdraw_phase_parse ( + struct WithdrawContext *wc, + const json_t *root) +{ + const json_t *j_denoms_h; + const json_t *j_coin_evs; + const char *cipher; + bool no_max_age; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("cipher", + &cipher), + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &wc->request.withdraw.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_mark_optional ( + GNUNET_JSON_spec_uint16 ("max_age", + &wc->request.withdraw.max_age), + &no_max_age), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("blinding_seed", + &wc->request.withdraw.blinding_seed), + &wc->request.withdraw.no_blinding_seed), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &wc->request.withdraw.reserve_sig), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (wc->rc->connection, + root, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + wc->phase = (GNUNET_SYSERR == res) + ? WITHDRAW_PHASE_RETURN_NO + : WITHDRAW_PHASE_RETURN_YES; + return; + } + + /* For now, we only support cipher "ED25519" for signatures by the reserve */ + if (0 != strcmp ("ED25519", + cipher)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN, + reserve_cipher_unknown, + cipher); + return; + } + + wc->request.withdraw.age_proof_required = ! no_max_age; + + if (wc->request.withdraw.age_proof_required) + { + /* The age value MUST be on the beginning of an age group */ + if (wc->request.withdraw.max_age != + TALER_get_lowest_age (&TEH_age_restriction_config.mask, + wc->request.withdraw.max_age)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL ( + wc, + WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "max_age must be the lower edge of an age group"); + return; + } + } + + /* validate array size */ + { + size_t num_coins = json_array_size (j_denoms_h); + size_t array_size = json_array_size (j_coin_evs); + const char *error; + + GNUNET_static_assert ( + TALER_MAX_COINS < INT_MAX / TALER_CNC_KAPPA); + +#define BAIL_IF(cond, msg) \ + if ((cond)) { \ + GNUNET_break_op (0); \ + error = (msg); break; \ + } + + do { + BAIL_IF (0 == num_coins, + "denoms_h must not be empty") + + /** + * 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! + */ + BAIL_IF (num_coins > TALER_MAX_COINS, + "maximum number of coins that can be withdrawn has been exceeded") + + BAIL_IF ((! wc->request.withdraw.age_proof_required) && + (num_coins != array_size), + "denoms_h and coin_evs must be arrays of the same size") + + BAIL_IF (wc->request.withdraw.age_proof_required && + ((TALER_CNC_KAPPA * num_coins) != array_size), + "coin_evs must be an array of length " + TALER_CNC_KAPPA_STR + "*len(denoms_h)") + + wc->request.withdraw.num_coins = num_coins; + wc->request.num_planchets = array_size; + error = NULL; + + } while (0); +#undef BAIL_IF + + if (NULL != error) + { + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + error); + return; + } + } + /* extract the denomination hashes */ + { + size_t idx; + json_t *value; + + wc->request.denoms_h + = GNUNET_new_array (wc->request.withdraw.num_coins, + struct TALER_DenominationHashP); + + json_array_foreach (j_denoms_h, idx, value) { + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &wc->request.denoms_h[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (wc->rc->connection, + value, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + wc->phase = (GNUNET_SYSERR == res) + ? WITHDRAW_PHASE_RETURN_NO + : WITHDRAW_PHASE_RETURN_YES; + return; + } + } + } + /* Parse the blinded coin envelopes */ + { + json_t *j_cev; + size_t idx; + + wc->request.planchets = + GNUNET_new_array (wc->request.num_planchets, + struct TALER_BlindedPlanchet); + json_array_foreach (j_coin_evs, idx, j_cev) + { + /* Now parse the individual envelopes and calculate the hash of + * the commitment along the way. */ + struct GNUNET_JSON_Specification kspec[] = { + TALER_JSON_spec_blinded_planchet (NULL, + &wc->request.planchets[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (wc->rc->connection, + j_cev, + kspec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + wc->phase = (GNUNET_SYSERR == res) + ? WITHDRAW_PHASE_RETURN_NO + : WITHDRAW_PHASE_RETURN_YES; + return; + } + + /* Check for duplicate planchets. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol */ + for (size_t i = 0; i < idx; i++) + { + if (0 == + TALER_blinded_planchet_cmp ( + &wc->request.planchets[idx], + &wc->request.planchets[i])) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_IDEMPOTENT_PLANCHET); + return; + } + } /* end duplicate check */ + } /* json_array_foreach over j_coin_evs */ + } /* scope of j_kappa_planchets, idx */ + wc->phase = WITHDRAW_PHASE_CHECK_KEYS; +} + + +MHD_RESULT +TEH_handler_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]) +{ + struct WithdrawContext *wc = rc->rh_ctx; + + (void) args; + if (NULL == wc) + { + wc = GNUNET_new (struct WithdrawContext); + rc->rh_ctx = wc; + rc->rh_cleaner = &clean_withdraw_rc; + wc->rc = rc; + wc->now = GNUNET_TIME_timestamp_get (); + } + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "withdrawal%s processing in phase %d\n", + wc->request.withdraw.age_proof_required + ? " (with required age proof)" + : "", + wc->phase); + switch (wc->phase) + { + case WITHDRAW_PHASE_PARSE: + withdraw_phase_parse (wc, + root); + break; + case WITHDRAW_PHASE_CHECK_KEYS: + phase_check_keys (wc); + break; + case WITHDRAW_PHASE_CHECK_RESERVE_SIGNATURE: + phase_check_reserve_signature (wc); + break; + case WITHDRAW_PHASE_RUN_LEGI_CHECK: + phase_run_legi_check (wc); + break; + case WITHDRAW_PHASE_SUSPENDED: + return MHD_YES; + case WITHDRAW_PHASE_CHECK_KYC_RESULT: + phase_check_kyc_result (wc); + break; + case WITHDRAW_PHASE_PREPARE_TRANSACTION: + phase_prepare_transaction (wc); + break; + case WITHDRAW_PHASE_RUN_TRANSACTION: + phase_run_transaction (wc); + break; + case WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS: + phase_generate_reply_success (wc); + break; + case WITHDRAW_PHASE_GENERATE_REPLY_ERROR: + phase_generate_reply_error (wc); + break; + case WITHDRAW_PHASE_RETURN_YES: + return MHD_YES; + case WITHDRAW_PHASE_RETURN_NO: + return MHD_NO; + } + } +} diff --git a/src/exchange/taler-exchange-httpd_post-withdraw.h b/src/exchange/taler-exchange-httpd_post-withdraw.h @@ -0,0 +1,61 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_post-withdraw.h + * @brief Handle /withdraw requests + * @note This endpoint was introduced in v26 of the protocol. + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_WITHDRAW_H +#define TALER_EXCHANGE_HTTPD_POST_WITHDRAW_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_withdraw_cleanup (void); + +/** + * @brief Handle a "/withdraw" request. + * @note This endpoint was introduced in v24 of the protocol. + * + * Parses the batch of requested "denom_pub" which specifies + * the key/value of the respective coin to be withdrawn, + * and checks the signature "reserve_sig" for given "reserve_pub" + * makes this a valid withdrawal request from the specific reserve. + * If the "max_age" value is set in the request, + * it is considered a commitment to withdraw age restricted coins. + * If the request is valid, the response contains a noreveal_index + * which the client has to use for the subsequent call to /reveal-withdraw. + * If "max_age" value is not set, and the request is valid, the envelopes + * with the blinded coins "blinded_coin_evs" is processed + * and the client receives the blinded signatures as response. + * + * @param rc request context + * @param root uploaded JSON data + * @param args array of additional options, not used. + * @return MHD result code + */ +MHD_RESULT +TEH_handler_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_purses_create.c b/src/exchange/taler-exchange-httpd_purses_create.c @@ -1,664 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_create.c - * @brief Handle /purses/$PID/create requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_common_deposit.h" -#include "taler-exchange-httpd_purses_create.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_exchangedb_lib.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for #create_transaction. - */ -struct PurseCreateContext -{ - - /** - * Total actually deposited by all the coins. - */ - struct TALER_Amount deposit_total; - - /** - * Our current time. - */ - struct GNUNET_TIME_Timestamp exchange_timestamp; - - /** - * Merge key for the purse. - */ - struct TALER_PurseMergePublicKeyP merge_pub; - - /** - * Encrypted contract of for the purse. - */ - struct TALER_EncryptedContract econtract; - - /** - * Signature of the client affiming this request. - */ - struct TALER_PurseContractSignatureP purse_sig; - - /** - * Fundamental details about the purse. - */ - struct TEH_PurseDetails pd; - - /** - * Array of coins being deposited. - */ - struct TEH_PurseDepositedCoin *coins; - - /** - * Length of the @e coins array. - */ - unsigned int num_coins; - - /** - * Minimum age for deposits into this purse. - */ - uint32_t min_age; - - /** - * Do we have an @e econtract? - */ - bool no_econtract; - -}; - - -/** - * Execute database transaction for /purses/$PID/create. 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 PurseCreateContext` - * @param connection MHD request context - * @param[out] mhd_ret set to MHD status on error - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -create_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct PurseCreateContext *pcc = cls; - enum GNUNET_DB_QueryStatus qs; - struct TALER_Amount purse_fee; - bool in_conflict = true; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &purse_fee)); - /* 1) create purse */ - qs = TEH_plugin->insert_purse_request ( - TEH_plugin->cls, - &pcc->pd.purse_pub, - &pcc->merge_pub, - pcc->pd.purse_expiration, - &pcc->pd.h_contract_terms, - pcc->min_age, - TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, - &purse_fee, - &pcc->pd.target_amount, - &pcc->purse_sig, - &in_conflict); - if (qs < 0) - { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - TALER_LOG_WARNING ( - "Failed to store create 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 create"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - 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, - &pcc->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); - 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_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) deposit all coins */ - for (unsigned int i = 0; i<pcc->num_coins; i++) - { - struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; - bool balance_ok = false; - bool conflict = true; - bool too_late = true; - - qs = TEH_make_coin_known (&coin->cpi, - connection, - &coin->known_coin_id, - mhd_ret); - if (qs < 0) - return qs; - qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls, - &pcc->pd.purse_pub, - &coin->cpi.coin_pub, - &coin->amount, - &coin->coin_sig, - &coin->amount_minus_fee, - &balance_ok, - &too_late, - &conflict); - if (qs <= 0) - { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - GNUNET_break (0 != qs); - TALER_LOG_WARNING ( - "Failed to store purse deposit information in database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "purse create deposit"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Coin %s has insufficient balance for purse deposit of amount %s\n", - TALER_B2S (&coin->cpi.coin_pub), - TALER_amount2s (&coin->amount)); - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &coin->cpi.denom_pub_hash, - &coin->cpi.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (too_late) - { - *mhd_ret - = TALER_MHD_reply_with_ec ( - connection, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "too late to deposit on purse creation"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (conflict) - { - struct TALER_Amount amount; - struct TALER_CoinSpendPublicKeyP coin_pub; - struct TALER_CoinSpendSignatureP coin_sig; - struct TALER_DenominationHashP h_denom_pub; - struct TALER_AgeCommitmentHashP phac; - char *partner_url = NULL; - - TEH_plugin->rollback (TEH_plugin->cls); - qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls, - &pcc->pd.purse_pub, - &coin->cpi.coin_pub, - &amount, - &h_denom_pub, - &phac, - &coin_sig, - &partner_url); - if (qs < 0) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - TALER_LOG_WARNING ( - "Failed to fetch purse deposit information from database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get purse deposit"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - *mhd_ret - = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_CONFLICT, - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), - GNUNET_JSON_pack_data_auto ("coin_pub", - &coin_pub), - GNUNET_JSON_pack_data_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &h_denom_pub), - GNUNET_JSON_pack_data_auto ("h_age_restrictions", - &phac), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("partner_url", - partner_url)), - TALER_JSON_pack_amount ("amount", - &amount)); - GNUNET_free (partner_url); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - /* 3) if present, persist contract */ - if (! pcc->no_econtract) - { - in_conflict = true; - qs = TEH_plugin->insert_contract (TEH_plugin->cls, - &pcc->pd.purse_pub, - &pcc->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 create 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, - &pcc->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; -} - - -/** - * Parse a coin and check signature of the coin and the denomination - * signature over the coin. - * - * @param[in,out] connection our HTTP connection - * @param[in,out] pcc request context - * @param[out] coin coin to initialize - * @param jcoin coin to parse - * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, - * #GNUNET_SYSERR on failure and no error could be returned - */ -static enum GNUNET_GenericReturnValue -parse_coin (struct MHD_Connection *connection, - struct PurseCreateContext *pcc, - struct TEH_PurseDepositedCoin *coin, - const json_t *jcoin) -{ - enum GNUNET_GenericReturnValue iret; - - if (GNUNET_OK != - (iret = TEH_common_purse_deposit_parse_coin (connection, - coin, - jcoin))) - return iret; - if (GNUNET_OK != - (iret = TEH_common_deposit_check_purse_deposit ( - connection, - coin, - &pcc->pd.purse_pub, - pcc->min_age))) - return iret; - if (0 > - TALER_amount_add (&pcc->deposit_total, - &pcc->deposit_total, - &coin->amount_minus_fee)) - { - GNUNET_break (0); - return (MHD_YES == - TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "total deposit contribution")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -MHD_RESULT -TEH_handler_purses_create ( - struct TEH_RequestContext *rc, - const struct TALER_PurseContractPublicKeyP *purse_pub, - const json_t *root) -{ - struct MHD_Connection *connection = rc->connection; - struct PurseCreateContext pcc = { - .pd.purse_pub = *purse_pub, - .exchange_timestamp = GNUNET_TIME_timestamp_get () - }; - const json_t *deposits; - json_t *deposit; - unsigned int idx; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("amount", - TEH_currency, - &pcc.pd.target_amount), - GNUNET_JSON_spec_uint32 ("min_age", - &pcc.min_age), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_econtract ("econtract", - &pcc.econtract), - &pcc.no_econtract), - GNUNET_JSON_spec_fixed_auto ("merge_pub", - &pcc.merge_pub), - GNUNET_JSON_spec_fixed_auto ("purse_sig", - &pcc.purse_sig), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &pcc.pd.h_contract_terms), - GNUNET_JSON_spec_array_const ("deposits", - &deposits), - GNUNET_JSON_spec_timestamp ("purse_expiration", - &pcc.pd.purse_expiration), - GNUNET_JSON_spec_end () - }; - const struct TEH_GlobalFee *gf; - - { - 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 */ - } - } - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &pcc.deposit_total)); - if (GNUNET_TIME_timestamp_cmp (pcc.pd.purse_expiration, - <, - pcc.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_PURSE_CREATE_EXPIRATION_BEFORE_NOW, - NULL); - } - if (GNUNET_TIME_absolute_is_never (pcc.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_PURSE_CREATE_EXPIRATION_IS_NEVER, - NULL); - } - pcc.num_coins = json_array_size (deposits); - if ( (0 == pcc.num_coins) || - (pcc.num_coins > TALER_MAX_COINS) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "deposits"); - } - { - 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); - } - gf = TEH_keys_global_fee_by_time (keys, - pcc.exchange_timestamp); - } - if (NULL == gf) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create 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); - } - /* parse deposits */ - pcc.coins = GNUNET_new_array (pcc.num_coins, - struct TEH_PurseDepositedCoin); - json_array_foreach (deposits, idx, deposit) - { - enum GNUNET_GenericReturnValue res; - struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; - - res = parse_coin (connection, - &pcc, - coin, - deposit); - if (GNUNET_OK != res) - { - for (unsigned int i = 0; i<idx; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - GNUNET_JSON_parse_free (spec); - return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - } - } - - if (0 < TALER_amount_cmp (&gf->fees.purse, - &pcc.deposit_total)) - { - GNUNET_break_op (0); - GNUNET_free (pcc.coins); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE, - NULL); - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - - if (GNUNET_OK != - TALER_wallet_purse_create_verify ( - pcc.pd.purse_expiration, - &pcc.pd.h_contract_terms, - &pcc.merge_pub, - pcc.min_age, - &pcc.pd.target_amount, - &pcc.pd.purse_pub, - &pcc.purse_sig)) - { - TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID, - NULL); - } - if ( (! pcc.no_econtract) && - (GNUNET_OK != - TALER_wallet_econtract_upload_verify (pcc.econtract.econtract, - pcc.econtract.econtract_size, - &pcc.econtract.contract_pub, - purse_pub, - &pcc.econtract.econtract_sig)) ) - { - TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - 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); - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - 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 create", - TEH_MT_REQUEST_PURSE_CREATE, - &mhd_ret, - &create_transaction, - &pcc)) - { - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - GNUNET_JSON_parse_free (spec); - return mhd_ret; - } - } - - /* generate regular response */ - { - MHD_RESULT res; - - res = TEH_RESPONSE_reply_purse_created (connection, - pcc.exchange_timestamp, - &pcc.deposit_total, - &pcc.pd); - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - GNUNET_JSON_parse_free (spec); - return res; - } -} - - -/* end of taler-exchange-httpd_purses_create.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_create.h b/src/exchange/taler-exchange-httpd_purses_create.h @@ -1,47 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_create.h - * @brief Handle /purses/$PID/create requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_PURSES_CREATE_H -#define TALER_EXCHANGE_HTTPD_PURSES_CREATE_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/purses/$PURSE_PUB/create" request. Parses the JSON, and, if - * successful, passes the JSON data to #create_transaction() to further check - * the details of the operation specified. If everything checks out, this - * will ultimately lead to the "purses create" being executed, or rejected. - * - * @param rc connection to handle - * @param purse_pub public key of the purse - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_purses_create ( - struct TEH_RequestContext *rc, - const struct TALER_PurseContractPublicKeyP *purse_pub, - const json_t *root); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_purses_delete.c b/src/exchange/taler-exchange-httpd_purses_delete.c @@ -1,149 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_delete.c - * @brief Handle DELETE /purses/$PID requests; parses the request and - * verifies the signature before handing deletion to the database. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_dbevents.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_common_deposit.h" -#include "taler-exchange-httpd_purses_delete.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_exchangedb_lib.h" -#include "taler-exchange-httpd_keys.h" - - -MHD_RESULT -TEH_handler_purses_delete ( - struct TEH_RequestContext *rc, - const char *const args[1]) -{ - struct MHD_Connection *connection = rc->connection; - struct TALER_PurseContractPublicKeyP purse_pub; - struct TALER_PurseContractSignatureP purse_sig; - bool found; - bool decided; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &purse_pub, - sizeof (purse_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, - args[0]); - } - TALER_MHD_parse_request_header_auto_t (connection, - "Taler-Purse-Signature", - &purse_sig); - if (GNUNET_OK != - TALER_wallet_purse_delete_verify (&purse_pub, - &purse_sig)) - { - TALER_LOG_WARNING ("Invalid signature on /purses/$PID/delete request\n"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID, - NULL); - } - if (GNUNET_SYSERR == - TEH_plugin->preflight (TEH_plugin->cls)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - "preflight failure"); - } - - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->do_purse_delete (TEH_plugin->cls, - &purse_pub, - &purse_sig, - &decided, - &found); - if (qs <= 0) - { - TALER_LOG_WARNING ( - "Failed to store delete purse information in database\n"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "purse delete"); - } - } - if (! found) - { - return TALER_MHD_reply_with_ec ( - connection, - TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, - NULL); - } - if (decided) - { - return TALER_MHD_reply_with_ec ( - connection, - TALER_EC_EXCHANGE_PURSE_DELETE_ALREADY_DECIDED, - NULL); - } - { - /* Possible minor optimization: integrate notification with - transaction above... */ - struct TALER_PurseEventP rep_deposited = { - .header.size = htons (sizeof (rep_deposited)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), - .purse_pub = purse_pub - }; - struct TALER_PurseEventP rep_merged = { - .header.size = htons (sizeof (rep_merged)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_MERGED), - .purse_pub = purse_pub - }; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying about purse deletion %s\n", - TALER_B2S (&purse_pub)); - TEH_plugin->event_notify (TEH_plugin->cls, - &rep_deposited.header, - NULL, - 0); - TEH_plugin->event_notify (TEH_plugin->cls, - &rep_merged.header, - NULL, - 0); - } - /* success */ - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-exchange-httpd_purses_delete.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_delete.h b/src/exchange/taler-exchange-httpd_purses_delete.h @@ -1,42 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_delete.h - * @brief Handle DELETE /purses/$PID requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_PURSES_DELETE_H -#define TALER_EXCHANGE_HTTPD_PURSES_DELETE_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a DELETE "/purses/$PURSE_PUB" request. - * - * @param rc request details about the request to handle - * @param args argument with the public key of the purse - * @return MHD result code - */ -MHD_RESULT -TEH_handler_purses_delete ( - struct TEH_RequestContext *rc, - const char *const args[1]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_purses_deposit.c b/src/exchange/taler-exchange-httpd_purses_deposit.c @@ -1,509 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_deposit.c - * @brief Handle /purses/$PID/deposit requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_dbevents.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_common_deposit.h" -#include "taler-exchange-httpd_purses_deposit.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_exchangedb_lib.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for #deposit_transaction. - */ -struct PurseDepositContext -{ - /** - * Public key of the purse we are creating. - */ - const struct TALER_PurseContractPublicKeyP *purse_pub; - - /** - * Total amount to be put into the purse. - */ - struct TALER_Amount amount; - - /** - * Total actually deposited by all the coins. - */ - struct TALER_Amount deposit_total; - - /** - * When should the purse expire. - */ - struct GNUNET_TIME_Timestamp purse_expiration; - - /** - * Hash of the contract (needed for signing). - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Our current time. - */ - struct GNUNET_TIME_Timestamp exchange_timestamp; - - /** - * Array of coins being deposited. - */ - struct TEH_PurseDepositedCoin *coins; - - /** - * Length of the @e coins array. - */ - unsigned int num_coins; - - /** - * Minimum age for deposits into this purse. - */ - uint32_t min_age; -}; - - -/** - * Send confirmation of purse creation success to client. - * - * @param connection connection to the client - * @param pcc details about the request that succeeded - * @return MHD result code - */ -static MHD_RESULT -reply_deposit_success (struct MHD_Connection *connection, - const struct PurseDepositContext *pcc) -{ - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec; - - if (TALER_EC_NONE != - (ec = TALER_exchange_online_purse_created_sign ( - &TEH_keys_exchange_sign_, - pcc->exchange_timestamp, - pcc->purse_expiration, - &pcc->amount, - &pcc->deposit_total, - pcc->purse_pub, - &pcc->h_contract_terms, - &pub, - &sig))) - { - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("total_deposited", - &pcc->deposit_total), - TALER_JSON_pack_amount ("purse_value_after_fees", - &pcc->amount), - GNUNET_JSON_pack_timestamp ("exchange_timestamp", - pcc->exchange_timestamp), - GNUNET_JSON_pack_timestamp ("purse_expiration", - pcc->purse_expiration), - GNUNET_JSON_pack_data_auto ("h_contract_terms", - &pcc->h_contract_terms), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); -} - - -/** - * Execute database transaction for /purses/$PID/deposit. 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 PurseDepositContext` - * @param connection MHD request context - * @param[out] mhd_ret set to MHD status on error - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -deposit_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct PurseDepositContext *pcc = cls; - enum GNUNET_DB_QueryStatus qs; - - qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - for (unsigned int i = 0; i<pcc->num_coins; i++) - { - struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; - bool balance_ok = false; - bool conflict = true; - bool too_late = true; - - qs = TEH_make_coin_known (&coin->cpi, - connection, - &coin->known_coin_id, - mhd_ret); - if (qs < 0) - return qs; - qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls, - pcc->purse_pub, - &coin->cpi.coin_pub, - &coin->amount, - &coin->coin_sig, - &coin->amount_minus_fee, - &balance_ok, - &too_late, - &conflict); - if (qs <= 0) - { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - GNUNET_break (0 != qs); - TALER_LOG_WARNING ( - "Failed to store purse deposit information in database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "do purse deposit"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &coin->cpi.denom_pub_hash, - &coin->cpi.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (too_late) - { - TEH_plugin->rollback (TEH_plugin->cls); - *mhd_ret - = TALER_MHD_reply_with_ec ( - connection, - TALER_EC_EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (conflict) - { - struct TALER_Amount amount; - struct TALER_CoinSpendPublicKeyP coin_pub; - struct TALER_CoinSpendSignatureP coin_sig; - struct TALER_DenominationHashP h_denom_pub; - struct TALER_AgeCommitmentHashP phac; - char *partner_url = NULL; - - TEH_plugin->rollback (TEH_plugin->cls); - qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls, - pcc->purse_pub, - &coin->cpi.coin_pub, - &amount, - &h_denom_pub, - &phac, - &coin_sig, - &partner_url); - if (qs < 0) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - TALER_LOG_WARNING ( - "Failed to fetch purse deposit information from database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get purse deposit"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - *mhd_ret - = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_CONFLICT, - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), - GNUNET_JSON_pack_data_auto ("coin_pub", - &coin_pub), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &h_denom_pub), - GNUNET_JSON_pack_data_auto ("h_age_commitment", - &phac), - GNUNET_JSON_pack_data_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("partner_url", - partner_url)), - TALER_JSON_pack_amount ("amount", - &amount)); - GNUNET_free (partner_url); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - return qs; -} - - -/** - * Parse a coin and check signature of the coin and the denomination - * signature over the coin. - * - * @param[in,out] connection our HTTP connection - * @param[in,out] pcc request context - * @param[out] coin coin to initialize - * @param jcoin coin to parse - * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, - * #GNUNET_SYSERR on failure and no error could be returned - */ -static enum GNUNET_GenericReturnValue -parse_coin (struct MHD_Connection *connection, - struct PurseDepositContext *pcc, - struct TEH_PurseDepositedCoin *coin, - const json_t *jcoin) -{ - enum GNUNET_GenericReturnValue iret; - - if (GNUNET_OK != - (iret = TEH_common_purse_deposit_parse_coin (connection, - coin, - jcoin))) - return iret; - if (GNUNET_OK != - (iret = TEH_common_deposit_check_purse_deposit ( - connection, - coin, - pcc->purse_pub, - pcc->min_age))) - return iret; - if (0 > - TALER_amount_add (&pcc->deposit_total, - &pcc->deposit_total, - &coin->amount_minus_fee)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "total deposit contribution"); - } - return GNUNET_OK; -} - - -MHD_RESULT -TEH_handler_purses_deposit ( - struct TEH_RequestContext *rc, - const struct TALER_PurseContractPublicKeyP *purse_pub, - const json_t *root) -{ - struct MHD_Connection *connection = rc->connection; - struct PurseDepositContext pcc = { - .purse_pub = purse_pub, - .exchange_timestamp = GNUNET_TIME_timestamp_get () - }; - const json_t *deposits; - json_t *deposit; - unsigned int idx; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("deposits", - &deposits), - 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 */ - } - } - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &pcc.deposit_total)); - pcc.num_coins = (unsigned int) json_array_size (deposits); - if ( (0 == pcc.num_coins) || - (((size_t) pcc.num_coins) != json_array_size (deposits)) || - (pcc.num_coins > TALER_MAX_COINS) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "deposits"); - } - - { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp create_timestamp; - struct GNUNET_TIME_Timestamp merge_timestamp; - bool was_deleted; - bool was_refunded; - - qs = TEH_plugin->select_purse ( - TEH_plugin->cls, - pcc.purse_pub, - &create_timestamp, - &pcc.purse_expiration, - &pcc.amount, - &pcc.deposit_total, - &pcc.h_contract_terms, - &merge_timestamp, - &was_deleted, - &was_refunded); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; /* handled below */ - } - if (was_refunded || - was_deleted) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_GONE, - was_deleted - ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED - : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, - GNUNET_TIME_timestamp2s (pcc.purse_expiration)); - } - } - - /* parse deposits */ - pcc.coins = GNUNET_new_array (pcc.num_coins, - struct TEH_PurseDepositedCoin); - json_array_foreach (deposits, idx, deposit) - { - enum GNUNET_GenericReturnValue res; - struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; - - res = parse_coin (connection, - &pcc, - coin, - deposit); - if (GNUNET_OK != res) - { - for (unsigned int i = 0; i<idx; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - } - } - - if (GNUNET_SYSERR == - TEH_plugin->preflight (TEH_plugin->cls)) - { - GNUNET_break (0); - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - 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 deposit", - TEH_MT_REQUEST_PURSE_DEPOSIT, - &mhd_ret, - &deposit_transaction, - &pcc)) - { - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - return mhd_ret; - } - } - { - struct TALER_PurseEventP rep = { - .header.size = htons (sizeof (rep)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), - .purse_pub = *pcc.purse_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying about purse deposit %s\n", - TALER_B2S (pcc.purse_pub)); - TEH_plugin->event_notify (TEH_plugin->cls, - &rep.header, - NULL, - 0); - } - - /* generate regular response */ - { - MHD_RESULT res; - - res = reply_deposit_success (connection, - &pcc); - for (unsigned int i = 0; i<pcc.num_coins; i++) - TEH_common_purse_deposit_free_coin (&pcc.coins[i]); - GNUNET_free (pcc.coins); - return res; - } -} - - -/* end of taler-exchange-httpd_purses_deposit.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_deposit.h b/src/exchange/taler-exchange-httpd_purses_deposit.h @@ -1,47 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_deposit.h - * @brief Handle /purses/$PID/deposit requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_PURSES_DEPOSIT_H -#define TALER_EXCHANGE_HTTPD_PURSES_DEPOSIT_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/purses/$PURSE_PUB/deposit" request. Parses the JSON, and, if - * successful, passes the JSON data to #deposit_transaction() to further check - * the details of the operation specified. If everything checks out, this - * will ultimately lead to the "purses deposit" being executed, or rejected. - * - * @param rc request to handle - * @param purse_pub public key of the purse - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_purses_deposit ( - struct TEH_RequestContext *rc, - const struct TALER_PurseContractPublicKeyP *purse_pub, - const json_t *root); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_purses_get.c b/src/exchange/taler-exchange-httpd_purses_get.c @@ -1,445 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_get.c - * @brief Handle GET /purses/$PID/$TARGET requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_mhd_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_purses_get.h" -#include "taler-exchange-httpd_mhd.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Information about an ongoing /purses GET operation. - */ -struct GetContext -{ - /** - * Kept in a DLL. - */ - struct GetContext *next; - - /** - * Kept in a DLL. - */ - struct GetContext *prev; - - /** - * Connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Subscription for the database event we are - * waiting for. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * Subscription for refund event we are - * waiting for. - */ - struct GNUNET_DB_EventHandler *ehr; - - /** - * Public key of our purse. - */ - struct TALER_PurseContractPublicKeyP purse_pub; - - /** - * When does this purse expire? - */ - struct GNUNET_TIME_Timestamp purse_expiration; - - /** - * When was this purse merged? - */ - struct GNUNET_TIME_Timestamp merge_timestamp; - - /** - * How much is the purse (supposed) to be worth? - */ - struct TALER_Amount amount; - - /** - * How much was deposited into the purse so far? - */ - struct TALER_Amount deposited; - - /** - * Hash over the contract of the purse. - */ - struct TALER_PrivateContractHashP h_contract; - - /** - * When will this request time out? - */ - struct GNUNET_TIME_Absolute timeout; - - /** - * true to wait for merge, false to wait for deposit. - */ - bool wait_for_merge; - - /** - * True if we are still suspended. - */ - bool suspended; -}; - - -/** - * Head of DLL of suspended GET requests. - */ -static struct GetContext *gc_head; - -/** - * Tail of DLL of suspended GET requests. - */ -static struct GetContext *gc_tail; - - -void -TEH_purses_get_cleanup () -{ - struct GetContext *gc; - - while (NULL != (gc = gc_head)) - { - GNUNET_CONTAINER_DLL_remove (gc_head, - gc_tail, - gc); - if (gc->suspended) - { - gc->suspended = false; - MHD_resume_connection (gc->connection); - } - } -} - - -/** - * Function called once a connection is done to - * clean up the `struct GetContext` state. - * - * @param rc context to clean up for - */ -static void -gc_cleanup (struct TEH_RequestContext *rc) -{ - struct GetContext *gc = rc->rh_ctx; - - GNUNET_assert (! gc->suspended); - if (NULL != gc->eh) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Cancelling DB event listening\n"); - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - gc->eh); - gc->eh = NULL; - } - if (NULL != gc->ehr) - { - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - gc->ehr); - gc->ehr = NULL; - } - GNUNET_free (gc); -} - - -/** - * Function called on events received from Postgres. - * Wakes up long pollers. - * - * @param cls the `struct TEH_RequestContext *` - * @param extra additional event data provided - * @param extra_size number of bytes in @a extra - */ -static void -db_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - struct TEH_RequestContext *rc = cls; - struct GetContext *gc = rc->rh_ctx; - struct GNUNET_AsyncScopeSave old_scope; - - (void) extra; - (void) extra_size; - if (NULL == gc) - return; /* event triggered while main transaction - was still running */ - if (! gc->suspended) - return; /* might get multiple wake-up events */ - gc->suspended = false; - GNUNET_async_scope_enter (&rc->async_scope_id, - &old_scope); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Waking up on %p - %p - %s\n", - rc, - gc, - gc->suspended ? "suspended" : "active"); - TEH_check_invariants (); - GNUNET_CONTAINER_DLL_remove (gc_head, - gc_tail, - gc); - MHD_resume_connection (gc->connection); - TALER_MHD_daemon_trigger (); - TEH_check_invariants (); - GNUNET_async_scope_restore (&old_scope); -} - - -MHD_RESULT -TEH_handler_purses_get (struct TEH_RequestContext *rc, - const char *const args[2]) -{ - struct GetContext *gc = rc->rh_ctx; - bool purse_deleted; - bool purse_refunded; - MHD_RESULT res; - - if (NULL == gc) - { - gc = GNUNET_new (struct GetContext); - rc->rh_ctx = gc; - rc->rh_cleaner = &gc_cleanup; - gc->connection = rc->connection; - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &gc->purse_pub, - sizeof (gc->purse_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, - args[0]); - } - if (0 == strcmp (args[1], - "merge")) - gc->wait_for_merge = true; - else if (0 == strcmp (args[1], - "deposit")) - gc->wait_for_merge = false; - else - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_PURSES_INVALID_WAIT_TARGET, - args[1]); - } - - TALER_MHD_parse_request_timeout (rc->connection, - &gc->timeout); - if ( (GNUNET_TIME_absolute_is_future (gc->timeout)) && - (NULL == gc->eh) ) - { - struct TALER_PurseEventP rep = { - .header.size = htons (sizeof (rep)), - .header.type = htons ( - gc->wait_for_merge - ? TALER_DBEVENT_EXCHANGE_PURSE_MERGED - : TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), - .purse_pub = gc->purse_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting DB event listening on purse %s (%s)\n", - TALER_B2S (&gc->purse_pub), - gc->wait_for_merge - ? "waiting for merge" - : "waiting for deposit"); - gc->eh = TEH_plugin->event_listen ( - TEH_plugin->cls, - GNUNET_TIME_absolute_get_remaining (gc->timeout), - &rep.header, - &db_event_cb, - rc); - if (NULL == gc->eh) - { - GNUNET_break (0); - gc->timeout = GNUNET_TIME_UNIT_ZERO_ABS; - } - else - { - struct GNUNET_DB_EventHeaderP repr = { - .size = htons (sizeof (repr)), - .type = htons (TALER_DBEVENT_EXCHANGE_PURSE_REFUNDED), - }; - - gc->ehr = TEH_plugin->event_listen ( - TEH_plugin->cls, - GNUNET_TIME_absolute_get_remaining (gc->timeout), - &repr, - &db_event_cb, - rc); - } - } - } /* end first-time initialization */ - - { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp create_timestamp; - - qs = TEH_plugin->select_purse (TEH_plugin->cls, - &gc->purse_pub, - &create_timestamp, - &gc->purse_expiration, - &gc->amount, - &gc->deposited, - &gc->h_contract, - &gc->merge_timestamp, - &purse_deleted, - &purse_refunded); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "select_purse %s returned %d (%s)\n", - args[0], - (int) qs, - GNUNET_TIME_timestamp2s (gc->merge_timestamp)); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_purse"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_purse"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; /* handled below */ - } - } - if (purse_refunded || - purse_deleted) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Purse refunded or deleted\n"); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_GONE, - purse_deleted - ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED - : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED - , - GNUNET_TIME_timestamp2s ( - gc->purse_expiration)); - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Deposited amount is %s (%d/%d/%d)\n", - TALER_amount2s (&gc->deposited), - GNUNET_TIME_absolute_is_future (gc->timeout), - GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time), - (0 < - TALER_amount_cmp (&gc->amount, - &gc->deposited))); - if (GNUNET_TIME_absolute_is_future (gc->timeout) && - ( ((gc->wait_for_merge) && - GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) || - ((! gc->wait_for_merge) && - (0 < - TALER_amount_cmp (&gc->amount, - &gc->deposited))) ) ) - { - gc->suspended = true; - GNUNET_CONTAINER_DLL_insert (gc_head, - gc_tail, - gc); - MHD_suspend_connection (gc->connection); - return MHD_YES; - } - - { - struct GNUNET_TIME_Timestamp dt = GNUNET_TIME_timestamp_get (); - struct TALER_ExchangePublicKeyP exchange_pub; - struct TALER_ExchangeSignatureP exchange_sig; - enum TALER_ErrorCode ec; - - if (GNUNET_TIME_timestamp_cmp (dt, - >, - gc->purse_expiration)) - dt = gc->purse_expiration; - if (0 < - TALER_amount_cmp (&gc->amount, - &gc->deposited)) - { - /* amount > deposited: not yet fully paid */ - dt = GNUNET_TIME_UNIT_ZERO_TS; - } - if (TALER_EC_NONE != - (ec = TALER_exchange_online_purse_status_sign ( - &TEH_keys_exchange_sign_, - gc->merge_timestamp, - dt, - &gc->deposited, - &exchange_pub, - &exchange_sig))) - { - res = TALER_MHD_reply_with_ec (rc->connection, - ec, - NULL); - } - else - { - /* Make sure merge_timestamp is omitted if not yet merged */ - if (GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) - gc->merge_timestamp = GNUNET_TIME_UNIT_ZERO_TS; - res = TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("balance", - &gc->deposited), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &exchange_pub), - GNUNET_JSON_pack_timestamp ("purse_expiration", - gc->purse_expiration), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("merge_timestamp", - gc->merge_timestamp)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("deposit_timestamp", - dt)) - ); - } - } - return res; -} - - -/* end of taler-exchange-httpd_purses_get.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_get.h b/src/exchange/taler-exchange-httpd_purses_get.h @@ -1,51 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_get.h - * @brief Handle /purses/$PURSE_PUB/$TARGET GET requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_PURSES_GET_H -#define TALER_EXCHANGE_HTTPD_PURSES_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Shutdown purses-get subsystem. Resumes all - * suspended long-polling clients and cleans up - * data structures. - */ -void -TEH_purses_get_cleanup (void); - - -/** - * Handle a GET "/purses/$PID/$TARGET" request. Parses the - * given "purse_pub" in @a args (which should contain the - * EdDSA public key of a purse) and then respond with the - * status of the purse. - * - * @param rc request context - * @param args array of additional options (length: 2, the purse_pub and a target) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_purses_get (struct TEH_RequestContext *rc, - const char *const args[2]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c @@ -1,815 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_merge.c - * @brief Handle /purses/$PID/merge requests; parses the POST and JSON and - * verifies the reserve signature before handing things off - * to the database. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_dbevents.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_purses_merge.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_exchangedb_lib.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for #merge_transaction. - */ -struct PurseMergeContext -{ - - /** - * Kept in a DLL. - */ - struct PurseMergeContext *next; - - /** - * Kept in a DLL. - */ - struct PurseMergeContext *prev; - - /** - * Our request. - */ - struct TEH_RequestContext *rc; - - /** - * Handle for the legitimization check. - */ - struct TEH_LegitimizationCheckHandle *lch; - - /** - * Fees that apply to this operation. - */ - const struct TALER_WireFeeSet *wf; - - /** - * Base URL of the exchange provider hosting the reserve. - */ - char *provider_url; - - /** - * URI of the account the purse is to be merged into. - * Must be of the form 'payto://taler-reserve/$EXCHANGE_URL/RESERVE_PUB'. - */ - struct TALER_NormalizedPayto payto_uri; - - /** - * Response to return, if set. - */ - struct MHD_Response *response; - - /** - * Public key of the purse we are creating. - */ - struct TALER_PurseContractPublicKeyP purse_pub; - - /** - * Total amount to be put into the purse. - */ - struct TALER_Amount target_amount; - - /** - * Current amount in the purse. - */ - struct TALER_Amount balance; - - /** - * When should the purse expire. - */ - struct GNUNET_TIME_Timestamp purse_expiration; - - /** - * When the client signed the merge. - */ - struct GNUNET_TIME_Timestamp merge_timestamp; - - /** - * Our current time. - */ - struct GNUNET_TIME_Timestamp exchange_timestamp; - - /** - * Merge key for the purse. - */ - struct TALER_PurseMergePublicKeyP merge_pub; - - /** - * Signature of the reservce affiming this request. - */ - struct TALER_ReserveSignatureP reserve_sig; - - /** - * Signature of the client affiming the merge. - */ - struct TALER_PurseMergeSignatureP merge_sig; - - /** - * Public key of the reserve (account), as extracted from @e payto_uri. - */ - union TALER_AccountPublicKeyP account_pub; - - /** - * Hash of the contract terms of the purse. - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Hash of the @e payto_uri. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * KYC status of the operation. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - - /** - * HTTP status to return with @e response, or 0. - */ - unsigned int http_status; - - /** - * Minimum age for deposits into this purse. - */ - uint32_t min_age; - - /** - * Set to true if this request was suspended. - */ - bool suspended; -}; - - -/** - * Kept in a DLL. - */ -static struct PurseMergeContext *pmc_head; - -/** - * Kept in a DLL. - */ -static struct PurseMergeContext *pmc_tail; - - -void -TEH_purses_merge_cleanup () -{ - struct PurseMergeContext *pmc; - - while (NULL != (pmc = pmc_head)) - { - GNUNET_CONTAINER_DLL_remove (pmc_head, - pmc_tail, - pmc); - MHD_resume_connection (pmc->rc->connection); - } -} - - -/** - * Function called with the result of a legitimization - * check. - * - * @param cls closure - * @param lcr legitimization check result - */ -static void -legi_result_cb ( - void *cls, - const struct TEH_LegitimizationCheckResult *lcr) -{ - struct PurseMergeContext *pmc = cls; - - pmc->lch = NULL; - MHD_resume_connection (pmc->rc->connection); - GNUNET_CONTAINER_DLL_remove (pmc_head, - pmc_tail, - pmc); - TALER_MHD_daemon_trigger (); - if (NULL != lcr->response) - { - pmc->response = lcr->response; - pmc->http_status = lcr->http_status; - return; - } - pmc->kyc = lcr->kyc; -} - - -/** - * Send confirmation of purse creation success to client. - * - * @param pmc details about the request that succeeded - * @return MHD result code - */ -static MHD_RESULT -reply_merge_success (const struct PurseMergeContext *pmc) -{ - struct MHD_Connection *connection = pmc->rc->connection; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec; - struct TALER_Amount merge_amount; - - if (0 < - TALER_amount_cmp (&pmc->balance, - &pmc->target_amount)) - { - GNUNET_break (0); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_JSON_pack_amount ("balance", - &pmc->balance), - TALER_JSON_pack_amount ("target_amount", - &pmc->target_amount)); - } - if ( (NULL == pmc->provider_url) || - (0 == strcmp (pmc->provider_url, - TEH_base_url)) ) - { - /* wad fee is always zero if we stay at our own exchange */ - merge_amount = pmc->target_amount; - } - else - { -#if WAD_NOT_IMPLEMENTED /* #7271 */ - /* FIXME: figure out partner, lookup wad fee by partner! #7271 */ - if (0 > - TALER_amount_subtract (&merge_amount, - &pmc->target_amount, - &wad_fee)) - { - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &merge_amount)); - } -#else - merge_amount = pmc->target_amount; -#endif - } - if (TALER_EC_NONE != - (ec = TALER_exchange_online_purse_merged_sign ( - &TEH_keys_exchange_sign_, - pmc->exchange_timestamp, - pmc->purse_expiration, - &merge_amount, - &pmc->purse_pub, - &pmc->h_contract_terms, - &pmc->account_pub.reserve_pub, - (NULL != pmc->provider_url) - ? pmc->provider_url - : TEH_base_url, - &pub, - &sig))) - { - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("merge_amount", - &merge_amount), - GNUNET_JSON_pack_timestamp ("exchange_timestamp", - pmc->exchange_timestamp), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); -} - - -/** - * 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 PurseMergeContext` - * @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 - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -amount_iterator (void *cls, - struct GNUNET_TIME_Absolute limit, - TALER_EXCHANGEDB_KycAmountCallback cb, - void *cb_cls) -{ - struct PurseMergeContext *pmc = cls; - enum GNUNET_GenericReturnValue ret; - enum GNUNET_DB_QueryStatus qs; - - ret = cb (cb_cls, - &pmc->target_amount, - GNUNET_TIME_absolute_get ()); - GNUNET_break (GNUNET_SYSERR != ret); - if (GNUNET_OK != ret) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - qs = TEH_plugin->select_merge_amounts_for_kyc_check ( - TEH_plugin->cls, - &pmc->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); - return qs; -} - - -/** - * Execute database transaction for /purses/$PID/merge. 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 PurseMergeContext` - * @param connection MHD request context - * @param[out] mhd_ret set to MHD status on error - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -merge_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct PurseMergeContext *pmc = cls; - enum GNUNET_DB_QueryStatus qs; - bool in_conflict = true; - bool no_balance = true; - bool no_partner = true; - - qs = TEH_plugin->do_purse_merge ( - TEH_plugin->cls, - &pmc->purse_pub, - &pmc->merge_sig, - pmc->merge_timestamp, - &pmc->reserve_sig, - pmc->provider_url, - &pmc->account_pub.reserve_pub, - &no_partner, - &no_balance, - &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, - "purse merge"); - return qs; - } - if (no_partner) - { - *mhd_ret = - TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN, - pmc->provider_url); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (no_balance) - { - *mhd_ret = - TALER_MHD_reply_with_error (connection, - MHD_HTTP_PAYMENT_REQUIRED, - TALER_EC_EXCHANGE_PURSE_NOT_FULL, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (in_conflict) - { - struct TALER_PurseMergeSignatureP merge_sig; - struct GNUNET_TIME_Timestamp merge_timestamp; - char *partner_url = NULL; - struct TALER_ReservePublicKeyP reserve_pub; - bool refunded; - - qs = TEH_plugin->select_purse_merge (TEH_plugin->cls, - &pmc->purse_pub, - &merge_sig, - &merge_timestamp, - &partner_url, - &reserve_pub, - &refunded); - if (qs <= 0) - { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - TALER_LOG_WARNING ( - "Failed to fetch merge 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 merge"); - return qs; - } - if (refunded) - { - 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; - } - if (0 != - GNUNET_memcmp (&merge_sig, - &pmc->merge_sig)) - { - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_CONFLICT, - GNUNET_JSON_pack_timestamp ("merge_timestamp", - merge_timestamp), - GNUNET_JSON_pack_data_auto ("merge_sig", - &merge_sig), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("partner_url", - partner_url)), - GNUNET_JSON_pack_data_auto ("reserve_pub", - &reserve_pub)); - GNUNET_free (partner_url); - return GNUNET_DB_STATUS_HARD_ERROR; - } - /* idempotent! */ - *mhd_ret = reply_merge_success (pmc); - GNUNET_free (partner_url); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - return qs; -} - - -/** - * Purse-merge-specific cleanup routine. Function called - * upon completion of the request that should - * clean up @a rh_ctx. Can be NULL. - * - * @param rc request context to clean up - */ -static void -clean_purse_merge_rc (struct TEH_RequestContext *rc) -{ - struct PurseMergeContext *pmc = rc->rh_ctx; - - if (NULL != pmc->lch) - { - TEH_legitimization_check_cancel (pmc->lch); - pmc->lch = NULL; - } - GNUNET_free (pmc->provider_url); - GNUNET_free (pmc); -} - - -MHD_RESULT -TEH_handler_purses_merge ( - struct TEH_RequestContext *rc, - const struct TALER_PurseContractPublicKeyP *purse_pub, - const json_t *root) -{ - struct PurseMergeContext *pmc = rc->rh_ctx; - - if (NULL == pmc) - { - pmc = GNUNET_new (struct PurseMergeContext); - rc->rh_ctx = pmc; - rc->rh_cleaner = &clean_purse_merge_rc; - pmc->rc = rc; - pmc->purse_pub = *purse_pub; - pmc->exchange_timestamp - = GNUNET_TIME_timestamp_get (); - - { - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_normalized_payto_uri ("payto_uri", - &pmc->payto_uri), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &pmc->reserve_sig), - GNUNET_JSON_spec_fixed_auto ("merge_sig", - &pmc->merge_sig), - GNUNET_JSON_spec_timestamp ("merge_timestamp", - &pmc->merge_timestamp), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (rc->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 */ - } - } - - { - struct TALER_PurseContractSignatureP purse_sig; - enum GNUNET_DB_QueryStatus qs; - - /* Fetch purse details */ - qs = TEH_plugin->get_purse_request ( - TEH_plugin->cls, - &pmc->purse_pub, - &pmc->merge_pub, - &pmc->purse_expiration, - &pmc->h_contract_terms, - &pmc->min_age, - &pmc->target_amount, - &pmc->balance, - &purse_sig); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse request"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse request"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* continued below */ - break; - } - } - - /* check signatures */ - if (GNUNET_OK != - TALER_wallet_purse_merge_verify ( - pmc->payto_uri, - pmc->merge_timestamp, - &pmc->purse_pub, - &pmc->merge_pub, - &pmc->merge_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE, - NULL); - } - - /* parse 'payto_uri' into pmc->account_pub and provider_url */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received payto: `%s'\n", - pmc->payto_uri.normalized_payto); - if ( (0 != strncmp (pmc->payto_uri.normalized_payto, - "payto://taler-reserve/", - strlen ("payto://taler-reserve/"))) && - (0 != strncmp (pmc->payto_uri.normalized_payto, - "payto://taler-reserve-http/", - strlen ("payto://taler-reserve-http/"))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payto_uri"); - } - - { - bool http; - const char *host; - const char *slash; - - http = (0 == strncmp (pmc->payto_uri.normalized_payto, - "payto://taler-reserve-http/", - strlen ("payto://taler-reserve-http/"))); - host = &pmc->payto_uri.normalized_payto[http - ? strlen ("payto://taler-reserve-http/") - : strlen ("payto://taler-reserve/")]; - slash = strchr (host, - '/'); - if (NULL == slash) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payto_uri"); - } - GNUNET_asprintf (&pmc->provider_url, - "%s://%.*s/", - http ? "http" : "https", - (int) (slash - host), - host); - slash++; - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data ( - slash, - strlen (slash), - &pmc->account_pub.reserve_pub, - sizeof (pmc->account_pub.reserve_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payto_uri"); - } - } - TALER_normalized_payto_hash (pmc->payto_uri, - &pmc->h_payto); - if (0 == strcmp (pmc->provider_url, - TEH_base_url)) - { - /* we use NULL to represent 'self' as the provider */ - GNUNET_free (pmc->provider_url); - } - else - { - char *method = GNUNET_strdup ("FIXME-WAD #7271"); - - /* FIXME-#7271: lookup wire method by pmc.provider_url! */ - pmc->wf = TEH_wire_fees_by_time (pmc->exchange_timestamp, - method); - if (NULL == pmc->wf) - { - MHD_RESULT res; - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot merge purse: wire fees not configured!\n"); - res = TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_WIRE_FEES_MISSING, - method); - GNUNET_free (method); - return res; - } - GNUNET_free (method); - } - - { - struct TALER_Amount zero_purse_fee; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero ( - pmc->target_amount.currency, - &zero_purse_fee)); - if (GNUNET_OK != - TALER_wallet_account_merge_verify ( - pmc->merge_timestamp, - &pmc->purse_pub, - pmc->purse_expiration, - &pmc->h_contract_terms, - &pmc->target_amount, - &zero_purse_fee, - pmc->min_age, - TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, - &pmc->account_pub.reserve_pub, - &pmc->reserve_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE, - NULL); - } - } - { - struct TALER_FullPayto fake_full_payto; - - GNUNET_asprintf (&fake_full_payto.full_payto, - "%s?receiver-name=wallet", - pmc->payto_uri.normalized_payto); - pmc->lch = TEH_legitimization_check ( - &rc->async_scope_id, - TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, - fake_full_payto, - &pmc->h_payto, - &pmc->account_pub, - &amount_iterator, - pmc, - &legi_result_cb, - pmc); - GNUNET_free (fake_full_payto.full_payto); - } - GNUNET_assert (NULL != pmc->lch); - MHD_suspend_connection (rc->connection); - GNUNET_CONTAINER_DLL_insert (pmc_head, - pmc_tail, - pmc); - return MHD_YES; - } - if (NULL != pmc->response) - { - return MHD_queue_response (rc->connection, - pmc->http_status, - pmc->response); - } - if (! pmc->kyc.ok) - return TEH_RESPONSE_reply_kyc_required ( - rc->connection, - &pmc->h_payto, - &pmc->kyc, - false); - - /* execute merge transaction */ - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "execute purse merge", - TEH_MT_REQUEST_PURSE_MERGE, - &mhd_ret, - &merge_transaction, - pmc)) - { - return mhd_ret; - } - } - - { - struct TALER_PurseEventP rep = { - .header.size = htons (sizeof (rep)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_MERGED), - .purse_pub = pmc->purse_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying about purse merge\n"); - TEH_plugin->event_notify (TEH_plugin->cls, - &rep.header, - NULL, - 0); - } - - /* generate regular response */ - return reply_merge_success (pmc); -} - - -/* end of taler-exchange-httpd_purses_merge.c */ diff --git a/src/exchange/taler-exchange-httpd_purses_merge.h b/src/exchange/taler-exchange-httpd_purses_merge.h @@ -1,54 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_purses_merge.h - * @brief Handle /purses/$PID/merge requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_PURSES_MERGE_H -#define TALER_EXCHANGE_HTTPD_PURSES_MERGE_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Resume suspended connections, we are shutting down. - */ -void -TEH_purses_merge_cleanup (void); - - -/** - * Handle a "/purses/$PURSE_PUB/merge" request. Parses the JSON, and, if - * successful, passes the JSON data to #merge_transaction() to further check - * the details of the operation specified. If everything checks out, this - * will ultimately lead to the "purses merge" being executed, or rejected. - * - * @param rc request to handle - * @param purse_pub public key of the purse - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_purses_merge ( - struct TEH_RequestContext *rc, - const struct TALER_PurseContractPublicKeyP *purse_pub, - const json_t *root); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_recoup-refresh.c b/src/exchange/taler-exchange-httpd_recoup-refresh.c @@ -1,431 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2017-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_recoup-refresh.c - * @brief Handle /recoup-refresh requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_db.h" -#include "taler-exchange-httpd_recoup-refresh.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" -#include "taler/taler_exchangedb_lib.h" - - -/** - * Closure for #recoup_refresh_transaction(). - */ -struct RecoupContext -{ - - /** - * Set by #recoup_transaction() to the old coin that will - * receive the recoup. - */ - struct TALER_CoinSpendPublicKeyP old_coin_pub; - - /** - * Details about the coin. - */ - const struct TALER_CoinPublicInfo *coin; - - /** - * Key used to blind the coin. - */ - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; - - /** - * Signature of the coin requesting recoup. - */ - const struct TALER_CoinSpendSignatureP *coin_sig; - - /** - * Unique ID of the coin in the known_coins table. - */ - uint64_t known_coin_id; - - /** - * Unique ID of the refresh reveal context of the melt for the new coin. - */ - uint64_t rrc_serial; - - /** - * Set by #recoup_transaction to the timestamp when the recoup - * was accepted. - */ - struct GNUNET_TIME_Timestamp now; - -}; - - -/** - * Execute a "recoup-refresh". The validity of the coin and signature have - * already been checked. The database must now check that the coin is not - * (double) spent, and execute the transaction. - * - * 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 the `struct RecoupContext *` - * @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 code - */ -static enum GNUNET_DB_QueryStatus -recoup_refresh_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct RecoupContext *pc = cls; - enum GNUNET_DB_QueryStatus qs; - bool recoup_ok; - bool internal_failure; - - /* Finally, store new refund data */ - pc->now = GNUNET_TIME_timestamp_get (); - qs = TEH_plugin->do_recoup_refresh (TEH_plugin->cls, - &pc->old_coin_pub, - pc->rrc_serial, - pc->coin_bks, - &pc->coin->coin_pub, - pc->known_coin_id, - pc->coin_sig, - &pc->now, - &recoup_ok, - &internal_failure); - 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_recoup_refresh"); - return qs; - } - - if (internal_failure) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "coin transaction history"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! recoup_ok) - { - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &pc->coin->denom_pub_hash, - &pc->coin->coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - return qs; -} - - -/** - * We have parsed the JSON information about the recoup request. Do - * some basic sanity checks (especially that the signature on the - * request and coin is valid) and then execute the recoup operation. - * Note that we need the DB to check the fee structure, so this is not - * done here but during the recoup_transaction(). - * - * @param connection the MHD connection to handle - * @param coin information about the coin - * @param exchange_vals values contributed by the exchange - * during refresh - * @param coin_bks blinding data of the coin (to be checked) - * @param nonce withdraw nonce (if CS is used) - * @param coin_sig signature of the coin - * @return MHD result code - */ -static MHD_RESULT -verify_and_execute_recoup_refresh ( - struct MHD_Connection *connection, - const struct TALER_CoinPublicInfo *coin, - const struct TALER_ExchangeBlindingValues *exchange_vals, - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, - const union GNUNET_CRYPTO_BlindSessionNonce *nonce, - const struct TALER_CoinSpendSignatureP *coin_sig) -{ - struct RecoupContext pc; - const struct TEH_DenominationKey *dk; - MHD_RESULT mret; - struct TALER_BlindedCoinHashP h_blind; - - /* check denomination exists and is in recoup mode */ - dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, - connection, - &mret); - if (NULL == dk) - return mret; - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) - { - /* This denomination is past the expiration time for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "RECOUP-REFRESH"); - } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) - { - /* This denomination is not yet valid */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "RECOUP-REFRESH"); - } - if (! dk->recoup_possible) - { - /* This denomination is not eligible for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE, - "RECOUP-REFRESH"); - } - - /* check denomination signature */ - switch (dk->denom_pub.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; - break; - case GNUNET_CRYPTO_BSA_CS: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; - break; - default: - break; - } - if (GNUNET_YES != - TALER_test_coin_valid (coin, - &dk->denom_pub)) - { - TALER_LOG_WARNING ("Invalid coin passed for recoup\n"); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL); - } - - /* check recoup request signature */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_recoup_refresh_verify (&coin->denom_pub_hash, - coin_bks, - &coin->coin_pub, - coin_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID, - NULL); - } - - { - struct TALER_CoinPubHashP c_hash; - struct TALER_BlindedPlanchet blinded_planchet; - - if (GNUNET_OK != - TALER_denom_blind (&dk->denom_pub, - coin_bks, - nonce, - &coin->h_age_commitment, - &coin->coin_pub, - exchange_vals, - &c_hash, - &blinded_planchet)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED, - NULL); - } - TALER_coin_ev_hash (&blinded_planchet, - &coin->denom_pub_hash, - &h_blind); - TALER_blinded_planchet_free (&blinded_planchet); - } - - pc.coin_sig = coin_sig; - pc.coin_bks = coin_bks; - pc.coin = coin; - - { - MHD_RESULT mhd_ret = MHD_NO; - enum GNUNET_DB_QueryStatus qs; - - /* make sure coin is 'known' in database */ - qs = TEH_make_coin_known (coin, - connection, - &pc.known_coin_id, - &mhd_ret); - /* no transaction => no serialization failures should be possible */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - if (qs < 0) - return mhd_ret; - } - - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_old_coin_by_h_blind (TEH_plugin->cls, - &h_blind, - &pc.old_coin_pub, - &pc.rrc_serial); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_old_coin_by_h_blind"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Recoup-refresh requested for unknown envelope %s\n", - GNUNET_h2s (&h_blind.hash)); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND, - NULL); - } - } - - /* Perform actual recoup transaction */ - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "run recoup-refresh", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &recoup_refresh_transaction, - &pc)) - return mhd_ret; - } - /* Recoup succeeded, return result */ - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "old_coin_pub", - &pc.old_coin_pub)); -} - - -/** - * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. - * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_recoup_refresh (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root) -{ - enum GNUNET_GenericReturnValue ret; - struct TALER_CoinPublicInfo coin = {0}; - union GNUNET_CRYPTO_BlindingSecretP coin_bks; - struct TALER_CoinSpendSignatureP coin_sig; - struct TALER_ExchangeBlindingValues exchange_vals; - union GNUNET_CRYPTO_BlindSessionNonce nonce; - bool no_nonce; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &coin.denom_pub_hash), - TALER_JSON_spec_denom_sig ("denom_sig", - &coin.denom_sig), - TALER_JSON_spec_exchange_blinding_values ("ewv", - &exchange_vals), - GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", - &coin_bks), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &coin.h_age_commitment), - &coin.no_age_commitment), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("nonce", - &nonce), - &no_nonce), - GNUNET_JSON_spec_end () - }; - - memset (&coin, - 0, - sizeof (coin)); - coin.coin_pub = *coin_pub; - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ - { - MHD_RESULT res; - - res = verify_and_execute_recoup_refresh (connection, - &coin, - &exchange_vals, - &coin_bks, - no_nonce - ? NULL - : &nonce, - &coin_sig); - GNUNET_JSON_parse_free (spec); - return res; - } -} - - -/* end of taler-exchange-httpd_recoup-refresh.c */ diff --git a/src/exchange/taler-exchange-httpd_recoup-refresh.h b/src/exchange/taler-exchange-httpd_recoup-refresh.h @@ -1,46 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2017, 2021 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_recoup-refresh.h - * @brief Handle /recoup-refresh requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RECOUP_REFRESH_H -#define TALER_EXCHANGE_HTTPD_RECOUP_REFRESH_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. - * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_recoup_refresh (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_recoup.c b/src/exchange/taler-exchange-httpd_recoup.c @@ -1,443 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2017-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_recoup.c - * @brief Handle /recoup requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_db.h" -#include "taler-exchange-httpd_recoup.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" -#include "taler/taler_exchangedb_lib.h" - -/** - * Closure for #recoup_transaction. - */ -struct RecoupContext -{ - /** - * Hash identifying the withdraw request. - */ - struct TALER_BlindedCoinHashP h_coin_ev; - - /** - * Set by #recoup_transaction() to the reserve that will - * receive the recoup, if #refreshed is #GNUNET_NO. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Details about the coin. - */ - const struct TALER_CoinPublicInfo *coin; - - /** - * Key used to blind the coin. - */ - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; - - /** - * Signature of the coin requesting recoup. - */ - const struct TALER_CoinSpendSignatureP *coin_sig; - - /** - * Unique ID of the withdraw operation in the withdraw table. - */ - uint64_t withdraw_serial_id; - - /** - * Unique ID of the coin in the known_coins table. - */ - uint64_t known_coin_id; - - /** - * Set by #recoup_transaction to the timestamp when the recoup - * was accepted. - */ - struct GNUNET_TIME_Timestamp now; - -}; - - -/** - * Execute a "recoup". The validity of the coin and signature have - * already been checked. The database must now check that the coin is - * not (double) spent, and execute the transaction. - * - * 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 the `struct RecoupContext *` - * @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 code - */ -static enum GNUNET_DB_QueryStatus -recoup_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct RecoupContext *pc = cls; - enum GNUNET_DB_QueryStatus qs; - bool recoup_ok; - bool internal_failure; - - /* Finally, store new refund data */ - pc->now = GNUNET_TIME_timestamp_get (); - qs = TEH_plugin->do_recoup (TEH_plugin->cls, - &pc->reserve_pub, - pc->withdraw_serial_id, - pc->coin_bks, - &pc->coin->coin_pub, - pc->known_coin_id, - pc->coin_sig, - &pc->now, - &recoup_ok, - &internal_failure); - 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_recoup"); - return qs; - } - - if (internal_failure) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "do_recoup"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! recoup_ok) - { - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &pc->coin->denom_pub_hash, - &pc->coin->coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - return qs; -} - - -/** - * We have parsed the JSON information about the recoup request. Do - * some basic sanity checks (especially that the signature on the - * request and coin is valid) and then execute the recoup operation. - * Note that we need the DB to check the fee structure, so this is not - * done here but during the recoup_transaction(). - * - * @param connection the MHD connection to handle - * @param coin information about the coin - * @param exchange_vals values contributed by the exchange - * during withdrawal - * @param coin_bks blinding data of the coin (to be checked) - * @param h_planchets The hash of the commitment of the original withdraw request - * @param nonce coin's nonce if CS is used - * @param coin_sig signature of the coin - * @return MHD result code - */ -static MHD_RESULT -verify_and_execute_recoup ( - struct MHD_Connection *connection, - const struct TALER_CoinPublicInfo *coin, - const struct TALER_ExchangeBlindingValues *exchange_vals, - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, - const struct TALER_HashBlindedPlanchetsP *h_planchets, - const union GNUNET_CRYPTO_BlindSessionNonce *nonce, - const struct TALER_CoinSpendSignatureP *coin_sig) -{ - struct RecoupContext pc; - const struct TEH_DenominationKey *dk; - MHD_RESULT mret; - - /* check denomination exists and is in recoup mode */ - dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, - connection, - &mret); - if (NULL == dk) - return mret; - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) - { - /* This denomination is past the expiration time for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "RECOUP"); - } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) - { - /* This denomination is not yet valid */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "RECOUP"); - } - if (! dk->recoup_possible) - { - /* This denomination is not eligible for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_RECOUP_NOT_ELIGIBLE, - "RECOUP"); - } - - /* check denomination signature */ - switch (dk->denom_pub.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; - break; - case GNUNET_CRYPTO_BSA_CS: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; - break; - default: - break; - } - if (GNUNET_YES != - TALER_test_coin_valid (coin, - &dk->denom_pub)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL); - } - - /* check recoup request signature */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_recoup_verify (&coin->denom_pub_hash, - coin_bks, - &coin->coin_pub, - coin_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID, - NULL); - } - - /* re-compute client-side blinding so we can - (a bit later) check that this coin was indeed - signed by us. */ - { - struct TALER_CoinPubHashP c_hash; - struct TALER_BlindedPlanchet blinded_planchet; - - if (GNUNET_OK != - TALER_denom_blind (&dk->denom_pub, - coin_bks, - nonce, - &coin->h_age_commitment, - &coin->coin_pub, - exchange_vals, - &c_hash, - &blinded_planchet)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, - NULL); - } - TALER_coin_ev_hash (&blinded_planchet, - &coin->denom_pub_hash, - &pc.h_coin_ev); - TALER_blinded_planchet_free (&blinded_planchet); - } - - pc.coin_sig = coin_sig; - pc.coin_bks = coin_bks; - pc.coin = coin; - - { - MHD_RESULT mhd_ret = MHD_NO; - enum GNUNET_DB_QueryStatus qs; - - /* make sure coin is 'known' in database */ - qs = TEH_make_coin_known (coin, - connection, - &pc.known_coin_id, - &mhd_ret); - /* no transaction => no serialization failures should be possible */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - if (qs < 0) - return mhd_ret; - } - - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_reserve_by_h_planchets ( - TEH_plugin->cls, - h_planchets, - &pc.reserve_pub, - &pc.withdraw_serial_id); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_reserve_by_commitment"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Recoup requested for unknown envelope %s\n", - GNUNET_h2s (&pc.h_coin_ev.hash)); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND, - NULL); - } - } - - /* Perform actual recoup transaction */ - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "run recoup", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &recoup_transaction, - &pc)) - return mhd_ret; - } - /* Recoup succeeded, return result */ - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "reserve_pub", - &pc.reserve_pub)); -} - - -/** - * Handle a "/coins/$COIN_PUB/recoup" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. - * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_recoup (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root) -{ - enum GNUNET_GenericReturnValue ret; - struct TALER_CoinPublicInfo coin; - union GNUNET_CRYPTO_BlindingSecretP coin_bks; - struct TALER_CoinSpendSignatureP coin_sig; - struct TALER_ExchangeBlindingValues exchange_vals; - struct TALER_HashBlindedPlanchetsP h_planchets; - union GNUNET_CRYPTO_BlindSessionNonce nonce; - bool no_nonce; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &coin.denom_pub_hash), - TALER_JSON_spec_denom_sig ("denom_sig", - &coin.denom_sig), - GNUNET_JSON_spec_fixed_auto ("h_planchets", - &h_planchets), - TALER_JSON_spec_exchange_blinding_values ("ewv", - &exchange_vals), - GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", - &coin_bks), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &coin.h_age_commitment), - &coin.no_age_commitment), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("nonce", - &nonce), - &no_nonce), - GNUNET_JSON_spec_end () - }; - - memset (&coin, - 0, - sizeof (coin)); - coin.coin_pub = *coin_pub; - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ - { - MHD_RESULT res; - - res = verify_and_execute_recoup (connection, - &coin, - &exchange_vals, - &coin_bks, - &h_planchets, - no_nonce - ? NULL - : &nonce, - &coin_sig); - GNUNET_JSON_parse_free (spec); - return res; - } -} - - -/* end of taler-exchange-httpd_recoup.c */ diff --git a/src/exchange/taler-exchange-httpd_recoup.h b/src/exchange/taler-exchange-httpd_recoup.h @@ -1,46 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2017 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_recoup.h - * @brief Handle /recoup requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RECOUP_H -#define TALER_EXCHANGE_HTTPD_RECOUP_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/coins/$COIN_PUB/recoup" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. - * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_recoup (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_refund.c b/src/exchange/taler-exchange-httpd_refund.c @@ -1,369 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_refund.c - * @brief Handle refund requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_refund.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * How often do we retry after soft database errors? - */ -#define MAX_RETRIES 3 - - -/** - * Generate successful refund confirmation message. - * - * @param connection connection to the client - * @param coin_pub public key of the coin - * @param refund details about the successful refund - * @return MHD result code - */ -static MHD_RESULT -reply_refund_success (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_EXCHANGEDB_RefundListEntry *refund) -{ - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec; - - if (TALER_EC_NONE != - (ec = TALER_exchange_online_refund_confirmation_sign ( - &TEH_keys_exchange_sign_, - &refund->h_contract_terms, - coin_pub, - &refund->merchant_pub, - refund->rtransaction_id, - &refund->refund_amount, - &pub, - &sig))) - { - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); -} - - -/** - * Closure for refund_transaction(). - */ -struct RefundContext -{ - /** - * Details about the deposit operation. - */ - const struct TALER_EXCHANGEDB_Refund *refund; - - /** - * Deposit fee of the coin. - */ - struct TALER_Amount deposit_fee; - - /** - * Unique ID of the coin in known_coins. - */ - uint64_t known_coin_id; -}; - - -/** - * Execute a "/refund" transaction. Returns a confirmation that the - * refund was successful, or a failure if we are not aware of a - * matching /deposit or if it is too late to do the refund. - * - * 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 closure with a `const struct TALER_EXCHANGEDB_Refund *` - * @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 -refund_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct RefundContext *rctx = cls; - const struct TALER_EXCHANGEDB_Refund *refund = rctx->refund; - enum GNUNET_DB_QueryStatus qs; - bool not_found; - bool refund_ok; - bool conflict; - bool gone; - - /* Finally, store new refund data */ - qs = TEH_plugin->do_refund (TEH_plugin->cls, - refund, - &rctx->deposit_fee, - rctx->known_coin_id, - &not_found, - &refund_ok, - &gone, - &conflict); - 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 refund"); - return qs; - } - - if (gone) - { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_GONE, - TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (conflict) - { - GNUNET_break_op (0); - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, - &refund->coin.denom_pub_hash, - &refund->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (not_found) - { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_REFUND_DEPOSIT_NOT_FOUND, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! refund_ok) - { - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT, - &refund->coin.denom_pub_hash, - &refund->coin.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - return qs; -} - - -/** - * We have parsed the JSON information about the refund, do some basic - * sanity checks (especially that the signature on the coin is valid) - * and then execute the refund. Note that we need the DB to check - * the fee structure, so this is not done here. - * - * @param connection the MHD connection to handle - * @param[in,out] refund information about the refund - * @return MHD result code - */ -static MHD_RESULT -verify_and_execute_refund (struct MHD_Connection *connection, - struct TALER_EXCHANGEDB_Refund *refund) -{ - struct RefundContext rctx = { - .refund = refund - }; - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_merchant_refund_verify (&refund->coin.coin_pub, - &refund->details.h_contract_terms, - refund->details.rtransaction_id, - &refund->details.refund_amount, - &refund->details.merchant_pub, - &refund->details.merchant_sig)) - { - TALER_LOG_WARNING ("Invalid signature on refund request\n"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID, - NULL); - } - - /* Fetch the coin's denomination (hash) */ - for (unsigned int i = 0; i < MAX_RETRIES; i++) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls, - &refund->coin.coin_pub, - &rctx.known_coin_id, - &refund->coin.denom_pub_hash); - switch (qs) - { - case GNUNET_DB_STATUS_SOFT_ERROR: - if (i < MAX_RETRIES - 1) - continue; - /* otherwise: fall-through */ - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_coin_denomination"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - { - MHD_RESULT res; - char *dhs; - - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - dhs = GNUNET_STRINGS_data_to_string_alloc ( - &refund->coin.denom_pub_hash, - sizeof (refund->coin.denom_pub_hash)); - res = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_REFUND_COIN_NOT_FOUND, - dhs); - GNUNET_free (dhs); - return res; - } - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - - { - /* Obtain information about the coin's denomination! */ - struct TEH_DenominationKey *dk; - MHD_RESULT mret; - - dk = TEH_keys_denomination_by_hash (&refund->coin.denom_pub_hash, - connection, - &mret); - if (NULL == dk) - { - /* DKI not found, but we do have a coin with this DK in our database; - not good... */ - GNUNET_break (0); - return mret; - } - refund->details.refund_fee = dk->meta.fees.refund; - rctx.deposit_fee = dk->meta.fees.deposit; - } - - /* Finally run the actual transaction logic */ - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "run refund", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &refund_transaction, - &rctx)) - { - return mhd_ret; - } - } - return reply_refund_success (connection, - &refund->coin.coin_pub, - &refund->details); -} - - -/** - * Handle a "/coins/$COIN_PUB/refund" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_refund() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. - * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_refund (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root) -{ - struct TALER_EXCHANGEDB_Refund refund = { - .details.refund_fee.currency = {0} /* set to invalid, just to be sure */ - }; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("refund_amount", - TEH_currency, - &refund.details.refund_amount), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &refund.details.h_contract_terms), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &refund.details.merchant_pub), - GNUNET_JSON_spec_uint64 ("rtransaction_id", - &refund.details.rtransaction_id), - GNUNET_JSON_spec_fixed_auto ("merchant_sig", - &refund.details.merchant_sig), - GNUNET_JSON_spec_end () - }; - - refund.coin.coin_pub = *coin_pub; - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == res) - return MHD_YES; /* failure */ - } - { - MHD_RESULT res; - - res = verify_and_execute_refund (connection, - &refund); - GNUNET_JSON_parse_free (spec); - return res; - } -} - - -/* end of taler-exchange-httpd_refund.c */ diff --git a/src/exchange/taler-exchange-httpd_refund.h b/src/exchange/taler-exchange-httpd_refund.h @@ -1,47 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014, 2015, 2016 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_refund.h - * @brief Handle /refund requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_REFUND_H -#define TALER_EXCHANGE_HTTPD_REFUND_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/coins/$COIN_PUB/refund" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_refund() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. - * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_refund (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_attest.c b/src/exchange/taler-exchange-httpd_reserves_attest.c @@ -1,391 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2022, 2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_attest.c - * @brief Handle /reserves/$RESERVE_PUB/attest requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_dbevents.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_reserves_attest.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * How far do we allow a client's time to be off when - * checking the request timestamp? - */ -#define TIMESTAMP_TOLERANCE \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) - - -/** - * Closure for #reserve_attest_transaction. - */ -struct ReserveAttestContext -{ - /** - * Public key of the reserve the inquiry is about. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Hash of the payto URI of this reserve. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * Timestamp of the request. - */ - struct GNUNET_TIME_Timestamp timestamp; - - /** - * Expiration time for the attestation. - */ - struct GNUNET_TIME_Timestamp etime; - - /** - * List of requested details. - */ - const json_t *details; - - /** - * Client signature approving the request. - */ - struct TALER_ReserveSignatureP reserve_sig; - - /** - * Attributes we are affirming. JSON object. - */ - json_t *json_attest; - - /** - * Database error codes encountered. - */ - enum GNUNET_DB_QueryStatus qs; - - /** - * Set to true if we did not find the reserve. - */ - bool not_found; - -}; - - -/** - * Send reserve attest to client. - * - * @param connection connection to the client - * @param rhc reserve attest to return - * @return MHD result code - */ -static MHD_RESULT -reply_reserve_attest_success (struct MHD_Connection *connection, - const struct ReserveAttestContext *rhc) -{ - struct TALER_ExchangeSignatureP exchange_sig; - struct TALER_ExchangePublicKeyP exchange_pub; - enum TALER_ErrorCode ec; - struct GNUNET_TIME_Timestamp now; - - if (NULL == rhc->json_attest) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - NULL); - } - now = GNUNET_TIME_timestamp_get (); - ec = TALER_exchange_online_reserve_attest_details_sign ( - &TEH_keys_exchange_sign_, - now, - rhc->etime, - &rhc->reserve_pub, - rhc->json_attest, - &exchange_pub, - &exchange_sig); - if (TALER_EC_NONE != ec) - { - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &exchange_pub), - GNUNET_JSON_pack_timestamp ("exchange_timestamp", - now), - GNUNET_JSON_pack_timestamp ("expiration_time", - rhc->etime), - GNUNET_JSON_pack_object_steal ("attributes", - rhc->json_attest)); -} - - -/** - * Function called with information about all applicable - * legitimization processes for the given user. Finds the - * available attributes and merges them into our result - * set based on the details requested by the client. - * - * @param cls our `struct ReserveAttestContext *` - * @param h_payto account for which the attribute data is stored - * @param provider_name provider that must be checked - * @param collection_time when was the data collected - * @param expiration_time when does the data expire - * @param enc_attributes_size number of bytes in @a enc_attributes - * @param enc_attributes encrypted attribute data - */ -static void -kyc_process_cb (void *cls, - const struct TALER_NormalizedPaytoHashP *h_payto, - const char *provider_name, - struct GNUNET_TIME_Timestamp collection_time, - struct GNUNET_TIME_Timestamp expiration_time, - size_t enc_attributes_size, - const void *enc_attributes) -{ - struct ReserveAttestContext *rsc = cls; - json_t *attrs; - json_t *val; - const char *name; - bool match = false; - - if (GNUNET_TIME_absolute_is_past (expiration_time.abs_time)) - return; - attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key, - enc_attributes, - enc_attributes_size); - if (NULL == attrs) - { - GNUNET_break (0); - return; - } - json_object_foreach (attrs, name, val) - { - bool requested = strcmp (name, - "FORM_ID"); /* we always return the FORM_ID */ - size_t idx; - json_t *str; - - if (NULL != json_object_get (rsc->json_attest, - name)) - continue; /* duplicate */ - json_array_foreach (rsc->details, idx, str) - { - if (0 == strcmp (json_string_value (str), - name)) - { - requested = true; - break; - } - } - if (! requested) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Skipping attribute `%s': not requested\n", - name); - continue; - } - match = true; - GNUNET_assert (0 == - json_object_set (rsc->json_attest, /* NOT set_new! */ - name, - val)); - } - json_decref (attrs); - if (! match) - return; - rsc->etime = GNUNET_TIME_timestamp_min (expiration_time, - rsc->etime); -} - - -/** - * Function implementing /reserves/$RID/attest transaction. Given the public - * key of a reserve, return the associated transaction attest. 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 ReserveAttestContext *` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!); unused - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -reserve_attest_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct ReserveAttestContext *rsc = cls; - enum GNUNET_DB_QueryStatus qs; - - rsc->json_attest = json_object (); - GNUNET_assert (NULL != rsc->json_attest); - qs = TEH_plugin->select_kyc_attributes (TEH_plugin->cls, - &rsc->h_payto, - &kyc_process_cb, - rsc); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - *mhd_ret - = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_kyc_attributes"); - return qs; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - rsc->not_found = true; - return qs; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - rsc->not_found = false; - break; - } - return qs; -} - - -MHD_RESULT -TEH_handler_reserves_attest (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[1]) -{ - struct ReserveAttestContext rsc = { - .etime = GNUNET_TIME_UNIT_FOREVER_TS - }; - MHD_RESULT mhd_ret; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_timestamp ("request_timestamp", - &rsc.timestamp), - GNUNET_JSON_spec_array_const ("details", - &rsc.details), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &rsc.reserve_sig), - GNUNET_JSON_spec_end () - }; - struct GNUNET_TIME_Timestamp now; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &rsc.reserve_pub, - sizeof (rsc.reserve_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, - args[0]); - } - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (rc->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 */ - } - } - now = GNUNET_TIME_timestamp_get (); - if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, - rsc.timestamp.abs_time, - TIMESTAMP_TOLERANCE)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, - NULL); - } - - if (GNUNET_OK != - TALER_wallet_reserve_attest_request_verify (rsc.timestamp, - rsc.details, - &rsc.reserve_pub, - &rsc.reserve_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE, - NULL); - } - - { - struct TALER_NormalizedPayto payto_uri; - - payto_uri = TALER_reserve_make_payto (TEH_base_url, - &rsc.reserve_pub); - TALER_normalized_payto_hash (payto_uri, - &rsc.h_payto); - GNUNET_free (payto_uri.normalized_payto); - } - - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "post reserve attest", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &reserve_attest_transaction, - &rsc)) - { - return mhd_ret; - } - if (rsc.not_found) - { - json_decref (rsc.json_attest); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - args[0]); - } - return reply_reserve_attest_success (rc->connection, - &rsc); -} - - -/* end of taler-exchange-httpd_reserves_attest.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_attest.h b/src/exchange/taler-exchange-httpd_reserves_attest.h @@ -1,41 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_attest.h - * @brief Handle /reserves/$RESERVE_PUB/attest requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RESERVES_ATTEST_H -#define TALER_EXCHANGE_HTTPD_RESERVES_ATTEST_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a POST "/reserves-attest/$RID" request. - * - * @param rc request context - * @param root uploaded body from the client - * @param args args[0] has public key of the reserve - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reserves_attest (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[1]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_close.c b/src/exchange/taler-exchange-httpd_reserves_close.c @@ -1,575 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_close.c - * @brief Handle /reserves/$RESERVE_PUB/close requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_reserves_close.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * How far do we allow a client's time to be off when - * checking the request timestamp? - */ -#define TIMESTAMP_TOLERANCE \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) - - -/** - * Closure for #reserve_close_transaction. - */ -struct ReserveCloseContext -{ - - /** - * Kept in a DLL. - */ - struct ReserveCloseContext *next; - - /** - * Kept in a DLL. - */ - struct ReserveCloseContext *prev; - - /** - * Our request context. - */ - struct TEH_RequestContext *rc; - - /** - * Handle for legitimization check. - */ - struct TEH_LegitimizationCheckHandle *lch; - - /** - * Where to wire the funds, may be NULL. - */ - struct TALER_FullPayto payto_uri; - - /** - * Response to return. Note that the response must - * be queued or destroyed by the callee. NULL - * if the legitimization check was successful and the handler should return - * a handler-specific result. - */ - struct MHD_Response *response; - - /** - * Public key of the reserve the inquiry is about. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Timestamp of the request. - */ - struct GNUNET_TIME_Timestamp timestamp; - - /** - * Client signature approving the request. - */ - struct TALER_ReserveSignatureP reserve_sig; - - /** - * Amount that will be wired (after closing fees). - */ - struct TALER_Amount wire_amount; - - /** - * Current balance of the reserve. - */ - struct TALER_Amount balance; - - /** - * Hash of the @e payto_uri, if given (otherwise zero). - */ - struct TALER_FullPaytoHashP h_payto; - - /** - * KYC status for the request. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - - /** - * Hash of the payto-URI that was used for the KYC decision. - */ - struct TALER_NormalizedPaytoHashP kyc_payto; - - /** - * Query status from the amount_it() helper function. - */ - enum GNUNET_DB_QueryStatus qs; - - /** - * HTTP status code for @a response, or 0 - */ - unsigned int http_status; - - /** - * Set to true if the request was suspended. - */ - bool suspended; - - /** - * Set to true if the request was suspended. - */ - bool resumed; -}; - - -/** - * Kept in a DLL. - */ -static struct ReserveCloseContext *rcc_head; - -/** - * Kept in a DLL. - */ -static struct ReserveCloseContext *rcc_tail; - - -void -TEH_reserves_close_cleanup () -{ - struct ReserveCloseContext *rcc; - - while (NULL != (rcc = rcc_head)) - { - GNUNET_CONTAINER_DLL_remove (rcc_head, - rcc_tail, - rcc); - MHD_resume_connection (rcc->rc->connection); - } -} - - -/** - * Send reserve close to client. - * - * @param rhc reserve close to return - * @return MHD result code - */ -static MHD_RESULT -reply_reserve_close_success ( - const struct ReserveCloseContext *rhc) -{ - struct MHD_Connection *connection = rhc->rc->connection; - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("wire_amount", - &rhc->wire_amount)); -} - - -/** - * Function called with the result of a legitimization - * check. - * - * @param cls closure - * @param lcr legitimization check result - */ -static void -reserve_close_legi_cb ( - void *cls, - const struct TEH_LegitimizationCheckResult *lcr) -{ - struct ReserveCloseContext *rcc = cls; - - rcc->lch = NULL; - rcc->http_status = lcr->http_status; - rcc->response = lcr->response; - rcc->kyc = lcr->kyc; - GNUNET_CONTAINER_DLL_remove (rcc_head, - rcc_tail, - rcc); - MHD_resume_connection (rcc->rc->connection); - rcc->resumed = true; - rcc->suspended = false; - TALER_MHD_daemon_trigger (); -} - - -/** - * 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 - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -amount_it (void *cls, - struct GNUNET_TIME_Absolute limit, - TALER_EXCHANGEDB_KycAmountCallback cb, - void *cb_cls) -{ - struct ReserveCloseContext *rcc = cls; - enum GNUNET_GenericReturnValue ret; - - ret = cb (cb_cls, - &rcc->balance, - GNUNET_TIME_absolute_get ()); - GNUNET_break (GNUNET_SYSERR != ret); - if (GNUNET_OK != ret) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - rcc->qs - = TEH_plugin->iterate_reserve_close_info ( - TEH_plugin->cls, - &rcc->kyc_payto, - limit, - cb, - cb_cls); - return rcc->qs; -} - - -/** - * Function implementing /reserves/$RID/close transaction. Given the public - * key of a reserve, return the associated transaction close. 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 ReserveCloseContext *` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!); unused - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -reserve_close_transaction ( - void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct ReserveCloseContext *rcc = cls; - enum GNUNET_DB_QueryStatus qs; - struct TALER_FullPayto payto_uri = { - .full_payto = NULL - }; - const struct TALER_WireFeeSet *wf; - - qs = TEH_plugin->select_reserve_close_info ( - TEH_plugin->cls, - &rcc->reserve_pub, - &rcc->balance, - &payto_uri); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - *mhd_ret - = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_reserve_close_info"); - return qs; - case GNUNET_DB_STATUS_SOFT_ERROR: - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - *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; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - if ( (NULL == rcc->payto_uri.full_payto) && - (NULL == payto_uri.full_payto) ) - { - *mhd_ret - = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if ( (! rcc->resumed) && - (NULL != rcc->payto_uri.full_payto) && - ( (NULL == payto_uri.full_payto) || - (0 != TALER_full_payto_cmp (payto_uri, - rcc->payto_uri)) ) ) - { - /* KYC check may be needed: we're not returning - the money to the account that funded the reserve - in the first place. */ - - TALER_full_payto_normalize_and_hash (rcc->payto_uri, - &rcc->kyc_payto); - rcc->lch = TEH_legitimization_check ( - &rcc->rc->async_scope_id, - TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE, - rcc->payto_uri, - &rcc->kyc_payto, - NULL, /* no account_pub: this is about the origin/destination account */ - &amount_it, - rcc, - &reserve_close_legi_cb, - rcc); - GNUNET_assert (NULL != rcc->lch); - GNUNET_CONTAINER_DLL_insert (rcc_head, - rcc_tail, - rcc); - MHD_suspend_connection (rcc->rc->connection); - rcc->suspended = true; - GNUNET_free (payto_uri.full_payto); - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - } - rcc->kyc.ok = true; - if (NULL == rcc->payto_uri.full_payto) - rcc->payto_uri = payto_uri; - - { - char *method; - - method = TALER_payto_get_method (rcc->payto_uri.full_payto); - wf = TEH_wire_fees_by_time (rcc->timestamp, - method); - if (NULL == wf) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, - method); - GNUNET_free (method); - GNUNET_free (payto_uri.full_payto); - return GNUNET_DB_STATUS_HARD_ERROR; - } - GNUNET_free (method); - } - - if (0 > - TALER_amount_subtract (&rcc->wire_amount, - &rcc->balance, - &wf->closing)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client attempted to close reserve with insufficient balance.\n"); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &rcc->wire_amount)); - *mhd_ret = reply_reserve_close_success (rcc); - GNUNET_free (payto_uri.full_payto); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - qs = TEH_plugin->insert_close_request ( - TEH_plugin->cls, - &rcc->reserve_pub, - payto_uri, - &rcc->reserve_sig, - rcc->timestamp, - &rcc->balance, - &wf->closing); - GNUNET_free (payto_uri.full_payto); - rcc->payto_uri.full_payto = NULL; - 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, - "insert_close_request"); - return qs; - } - if (qs <= 0) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - return qs; - } - return qs; -} - - -/** - * Cleanup routine. Function called - * upon completion of the request that should - * clean up @a rh_ctx. Can be NULL. - * - * @param rc request to clean up context for - */ -static void -reserve_close_cleanup (struct TEH_RequestContext *rc) -{ - struct ReserveCloseContext *rcc = rc->rh_ctx; - - if (NULL != rcc->lch) - { - TEH_legitimization_check_cancel (rcc->lch); - rcc->lch = NULL; - } - GNUNET_free (rcc); -} - - -MHD_RESULT -TEH_handler_reserves_close ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - struct ReserveCloseContext *rcc = rc->rh_ctx; - MHD_RESULT mhd_ret; - - if (NULL == rcc) - { - rcc = GNUNET_new (struct ReserveCloseContext); - rc->rh_ctx = rcc; - rc->rh_cleaner = &reserve_close_cleanup; - rcc->reserve_pub = *reserve_pub; - rcc->rc = rc; - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_timestamp ("request_timestamp", - &rcc->timestamp), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_full_payto_uri ("payto_uri", - &rcc->payto_uri), - NULL), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &rcc->reserve_sig), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (rc->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 */ - } - } - } - - { - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); - if (! GNUNET_TIME_absolute_approx_eq ( - now.abs_time, - rcc->timestamp.abs_time, - TIMESTAMP_TOLERANCE)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, - NULL); - } - } - - if (NULL != rcc->payto_uri.full_payto) - TALER_full_payto_hash (rcc->payto_uri, - &rcc->h_payto); - if (GNUNET_OK != - TALER_wallet_reserve_close_verify ( - rcc->timestamp, - &rcc->h_payto, - &rcc->reserve_pub, - &rcc->reserve_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE, - NULL); - } - } - if (NULL != rcc->response) - return MHD_queue_response (rc->connection, - rcc->http_status, - rcc->response); - if (rcc->resumed && - (! rcc->kyc.ok) ) - { - if (0 == rcc->kyc.requirement_row) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "requirement row not set"); - } - return TEH_RESPONSE_reply_kyc_required ( - rc->connection, - &rcc->kyc_payto, - &rcc->kyc, - false); - } - - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "reserve close", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &reserve_close_transaction, - rcc)) - { - return mhd_ret; - } - if (rcc->suspended) - return MHD_YES; - return reply_reserve_close_success (rcc); -} - - -/* end of taler-exchange-httpd_reserves_close.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_close.h b/src/exchange/taler-exchange-httpd_reserves_close.h @@ -1,48 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_close.h - * @brief Handle /reserves/$RESERVE_PUB/close requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RESERVES_CLOSE_H -#define TALER_EXCHANGE_HTTPD_RESERVES_CLOSE_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Resume suspended connections, we are shutting down. - */ -void -TEH_reserves_close_cleanup (void); - -/** - * Handle a POST "/reserves/$RID/close" request. - * - * @param rc request context - * @param reserve_pub public key of the reserve - * @param root uploaded body from the client - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reserves_close ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_get.c b/src/exchange/taler-exchange-httpd_reserves_get.c @@ -1,278 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_get.c - * @brief Handle /reserves/$RESERVE_PUB GET requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_mhd_lib.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_reserves_get.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Reserve GET request that is long-polling. - */ -struct ReservePoller -{ - /** - * Kept in a DLL. - */ - struct ReservePoller *next; - - /** - * Kept in a DLL. - */ - struct ReservePoller *prev; - - /** - * Connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Our request context. - */ - struct TEH_RequestContext *rc; - - /** - * Subscription for the database event we are waiting for. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * When will this request time out? - */ - struct GNUNET_TIME_Absolute timeout; - - /** - * Public key of the reserve the inquiry is about. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Balance of the reserve, set in the callback. - */ - struct TALER_Amount balance; - - /** - * Last origin account of the reserve, NULL if only - * P2P payments were made. - */ - struct TALER_FullPayto origin_account; - - /** - * True if we are still suspended. - */ - bool suspended; - -}; - - -/** - * Head of list of requests in long polling. - */ -static struct ReservePoller *rp_head; - -/** - * Tail of list of requests in long polling. - */ -static struct ReservePoller *rp_tail; - - -void -TEH_reserves_get_cleanup () -{ - for (struct ReservePoller *rp = rp_head; - NULL != rp; - rp = rp->next) - { - if (rp->suspended) - { - rp->suspended = false; - MHD_resume_connection (rp->connection); - } - } -} - - -/** - * Function called once a connection is done to - * clean up the `struct ReservePoller` state. - * - * @param rc context to clean up for - */ -static void -rp_cleanup (struct TEH_RequestContext *rc) -{ - struct ReservePoller *rp = rc->rh_ctx; - - GNUNET_assert (! rp->suspended); - if (NULL != rp->eh) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Cancelling DB event listening on cleanup (odd unless during shutdown)\n"); - TEH_plugin->event_listen_cancel (TEH_plugin->cls, - rp->eh); - rp->eh = NULL; - } - GNUNET_CONTAINER_DLL_remove (rp_head, - rp_tail, - rp); - GNUNET_free (rp->origin_account.full_payto); - GNUNET_free (rp); -} - - -/** - * Function called on events received from Postgres. - * Wakes up long pollers. - * - * @param cls the `struct TEH_RequestContext *` - * @param extra additional event data provided - * @param extra_size number of bytes in @a extra - */ -static void -db_event_cb (void *cls, - const void *extra, - size_t extra_size) -{ - struct ReservePoller *rp = cls; - struct GNUNET_AsyncScopeSave old_scope; - - (void) extra; - (void) extra_size; - if (! rp->suspended) - return; /* might get multiple wake-up events */ - GNUNET_async_scope_enter (&rp->rc->async_scope_id, - &old_scope); - TEH_check_invariants (); - rp->suspended = false; - MHD_resume_connection (rp->connection); - TALER_MHD_daemon_trigger (); - TEH_check_invariants (); - GNUNET_async_scope_restore (&old_scope); -} - - -MHD_RESULT -TEH_handler_reserves_get ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub) -{ - struct ReservePoller *rp = rc->rh_ctx; - - if (NULL == rp) - { - rp = GNUNET_new (struct ReservePoller); - rp->connection = rc->connection; - rp->rc = rc; - rc->rh_ctx = rp; - rc->rh_cleaner = &rp_cleanup; - GNUNET_CONTAINER_DLL_insert (rp_head, - rp_tail, - rp); - rp->reserve_pub = *reserve_pub; - TALER_MHD_parse_request_timeout (rc->connection, - &rp->timeout); - } - - if ( (GNUNET_TIME_absolute_is_future (rp->timeout)) && - (NULL == rp->eh) ) - { - struct TALER_ReserveEventP rep = { - .header.size = htons (sizeof (rep)), - .header.type = htons (TALER_DBEVENT_EXCHANGE_RESERVE_INCOMING), - .reserve_pub = rp->reserve_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting DB event listening until %s\n", - GNUNET_TIME_absolute2s (rp->timeout)); - rp->eh = TEH_plugin->event_listen ( - TEH_plugin->cls, - GNUNET_TIME_absolute_get_remaining (rp->timeout), - &rep.header, - &db_event_cb, - rp); - } - { - enum GNUNET_DB_QueryStatus qs; - - GNUNET_free (rp->origin_account.full_payto); - qs = TEH_plugin->get_reserve_balance (TEH_plugin->cls, - &rp->reserve_pub, - &rp->balance, - &rp->origin_account); - switch (qs) - { - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); /* single-shot query should never have soft-errors */ - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "get_reserve_balance"); - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_reserve_balance"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got reserve balance of %s\n", - TALER_amount2s (&rp->balance)); - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "last_origin", - rp->origin_account.full_payto)), - TALER_JSON_pack_amount ("balance", - &rp->balance)); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - if (! GNUNET_TIME_absolute_is_future (rp->timeout)) - { - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - NULL); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Long-polling on reserve for %s\n", - GNUNET_STRINGS_relative_time_to_string ( - GNUNET_TIME_absolute_get_remaining (rp->timeout), - true)); - rp->suspended = true; - MHD_suspend_connection (rc->connection); - return MHD_YES; - } - } - GNUNET_break (0); - return MHD_NO; -} - - -/* end of taler-exchange-httpd_reserves_get.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_get.h b/src/exchange/taler-exchange-httpd_reserves_get.h @@ -1,54 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_get.h - * @brief Handle /reserves/$RESERVE_PUB GET requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RESERVES_GET_H -#define TALER_EXCHANGE_HTTPD_RESERVES_GET_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Shutdown reserves-get subsystem. Resumes all - * suspended long-polling clients and cleans up - * data structures. - */ -void -TEH_reserves_get_cleanup (void); - - -/** - * Handle a GET "/reserves/" request. Parses the - * given "reserve_pub" in @a args (which should contain the - * EdDSA public key of a reserve) and then respond with the - * status of the reserve. - * - * @param rc request context - * @param reserve_pub public key of the reserve - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reserves_get ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_get_attest.c b/src/exchange/taler-exchange-httpd_reserves_get_attest.c @@ -1,195 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_get_attest.c - * @brief Handle GET /reserves/$RESERVE_PUB/attest requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_reserves_get_attest.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Closure for #reserve_attest_transaction. - */ -struct ReserveAttestContext -{ - /** - * Public key of the reserve the inquiry is about. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Hash of the payto URI of this reserve. - */ - struct TALER_NormalizedPaytoHashP h_payto; - - /** - * Available attributes. - */ - json_t *attributes; - -}; - - -/** - * Function called with information about all applicable - * legitimization processes for the given user. - * - * @param cls our `struct ReserveAttestContext *` - * @param h_payto account for which the attribute data is stored - * @param provider_name provider that must be checked - * @param collection_time when was the data collected - * @param expiration_time when does the data expire - * @param enc_attributes_size number of bytes in @a enc_attributes - * @param enc_attributes encrypted attribute data - */ -static void -kyc_process_cb (void *cls, - const struct TALER_NormalizedPaytoHashP *h_payto, - const char *provider_name, - struct GNUNET_TIME_Timestamp collection_time, - struct GNUNET_TIME_Timestamp expiration_time, - size_t enc_attributes_size, - const void *enc_attributes) -{ - struct ReserveAttestContext *rsc = cls; - json_t *attrs; - json_t *val; - const char *name; - - if (GNUNET_TIME_absolute_is_past ( - expiration_time.abs_time)) - return; - attrs = TALER_CRYPTO_kyc_attributes_decrypt ( - &TEH_attribute_key, - enc_attributes, - enc_attributes_size); - json_object_foreach (attrs, name, val) - { - bool duplicate = false; - size_t idx; - json_t *str; - - json_array_foreach (rsc->attributes, idx, str) - { - if (0 == strcmp (json_string_value (str), - name)) - { - duplicate = true; - break; - } - } - if (duplicate) - continue; - GNUNET_assert (0 == - json_array_append_new (rsc->attributes, - json_string (name))); - } - json_decref (attrs); -} - - -MHD_RESULT -TEH_handler_reserves_get_attest ( - struct TEH_RequestContext *rc, - const char *const args[1]) -{ - struct ReserveAttestContext rsc = { - .attributes = NULL - }; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data ( - args[0], - strlen (args[0]), - &rsc.reserve_pub, - sizeof (rsc.reserve_pub))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, - args[0]); - } - { - struct TALER_NormalizedPayto payto_uri; - - payto_uri - = TALER_reserve_make_payto (TEH_base_url, - &rsc.reserve_pub); - TALER_normalized_payto_hash (payto_uri, - &rsc.h_payto); - GNUNET_free (payto_uri.normalized_payto); - } - { - enum GNUNET_DB_QueryStatus qs; - - rsc.attributes = json_array (); - GNUNET_assert (NULL != rsc.attributes); - qs = TEH_plugin->select_kyc_attributes (TEH_plugin->cls, - &rsc.h_payto, - &kyc_process_cb, - &rsc); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - json_decref (rsc.attributes); - rsc.attributes = NULL; - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_kyc_attributes"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - json_decref (rsc.attributes); - rsc.attributes = NULL; - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_kyc_attributes"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break_op (0); - json_decref (rsc.attributes); - rsc.attributes = NULL; - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("details", - rsc.attributes)); -} - - -/* end of taler-exchange-httpd_reserves_get_attest.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_get_attest.h b/src/exchange/taler-exchange-httpd_reserves_get_attest.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_get_attest.h - * @brief Handle /reserves/$RESERVE_PUB GET_ATTEST requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RESERVES_GET_ATTEST_H -#define TALER_EXCHANGE_HTTPD_RESERVES_GET_ATTEST_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/reserves/$RID/attest" request. Parses the - * given "reserve_pub" in @a args (which should contain the - * EdDSA public key of a reserve) and then responds with the - * available attestations for the reserve. - * - * @param rc request context - * @param args array of additional options (length: 1, just the reserve_pub) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reserves_get_attest (struct TEH_RequestContext *rc, - const char *const args[1]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_history.c b/src/exchange/taler-exchange-httpd_reserves_history.c @@ -1,633 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_history.c - * @brief Handle /reserves/$RESERVE_PUB HISTORY requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_reserves_history.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Compile the history of a reserve into a JSON object. - * - * @param rh reserve history to JSON-ify - * @return json representation of the @a rh, NULL on error - */ -static json_t * -compile_reserve_history ( - const struct TALER_EXCHANGEDB_ReserveHistory *rh) -{ - json_t *json_history; - - json_history = json_array (); - GNUNET_assert (NULL != json_history); - for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh; - NULL != pos; - pos = pos->next) - { - switch (pos->type) - { - case TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE: - { - const struct TALER_EXCHANGEDB_BankTransfer *bank = - pos->details.bank; - - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "CREDIT"), - GNUNET_JSON_pack_timestamp ("timestamp", - bank->execution_date), - TALER_JSON_pack_full_payto ("sender_account_url", - bank->sender_account_details), - GNUNET_JSON_pack_uint64 ("wire_reference", - bank->wire_reference), - TALER_JSON_pack_amount ("amount", - &bank->amount)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - break; - } - case TALER_EXCHANGEDB_RO_WITHDRAW_COINS: - { - const struct TALER_EXCHANGEDB_Withdraw *withdraw - = pos->details.withdraw; - struct TALER_Amount withdraw_fee; - struct TEH_KeyStateHandle *ksh; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero ( - TEH_currency, - &withdraw_fee)); - - /* - * We need to calculate the fees for the withdraw. - * Therefore, we need to get access to the key state. - */ - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - - for (size_t i = 0; i < withdraw->num_coins; i++) - { - /* Find the denomination and accumulate the fee */ - { - struct TEH_DenominationKey *dk; - dk = TEH_keys_denomination_by_hash_from_state ( - ksh, - &withdraw->denom_pub_hashes[i], - NULL, - NULL); - - if (NULL == dk) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - - if (0 > TALER_amount_add (&withdraw_fee, - &withdraw_fee, - &dk->meta.fees.withdraw)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - } - - /* Prepare the entry for the history */ - { - json_t *j_entry = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ( - "type", - "WITHDRAW"), - GNUNET_JSON_pack_data_auto ( - "reserve_sig", - &withdraw->reserve_sig), - GNUNET_JSON_pack_data_auto ( - "planchets_h", - &withdraw->planchets_h), - GNUNET_JSON_pack_uint64 ( - "num_coins", - withdraw->num_coins), - TALER_JSON_pack_array_of_data ( - "denom_pub_hashes", - withdraw->num_coins, - withdraw->denom_pub_hashes, - sizeof(withdraw->denom_pub_hashes[0])), - TALER_JSON_pack_amount ( - "withdraw_fee", - &withdraw_fee), - TALER_JSON_pack_amount ( - "amount", - &withdraw->amount_with_fee) - ); - - if (! withdraw->no_blinding_seed) - { - if (0!= - json_object_update_new ( - j_entry, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ( - "blinding_seed", - &withdraw->blinding_seed)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - if (0!= - json_object_update_new ( - j_entry, - GNUNET_JSON_PACK ( - TALER_JSON_pack_array_of_data ( - "cs_r_values", - withdraw->num_cs_r_values, - withdraw->cs_r_values, - sizeof(withdraw->cs_r_values[0]))))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - if (withdraw->age_proof_required) - { - if (0 != - json_object_update_new ( - j_entry, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ( - "noreveal_index", - withdraw->noreveal_index), - GNUNET_JSON_pack_data_auto ( - "selected_h", - &withdraw->selected_h), - GNUNET_JSON_pack_uint64 ( - "max_age", - withdraw->max_age)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - - if (0 != - json_array_append_new ( - json_history, - j_entry)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - } - - break; - case TALER_EXCHANGEDB_RO_RECOUP_COIN: - { - const struct TALER_EXCHANGEDB_Recoup *recoup - = pos->details.recoup; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - - if (TALER_EC_NONE != - TALER_exchange_online_confirm_recoup_sign ( - &TEH_keys_exchange_sign_, - recoup->timestamp, - &recoup->value, - &recoup->coin.coin_pub, - &recoup->reserve_pub, - &pub, - &sig)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "RECOUP"), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_timestamp ("timestamp", - recoup->timestamp), - TALER_JSON_pack_amount ("amount", - &recoup->value), - GNUNET_JSON_pack_data_auto ("coin_pub", - &recoup->coin.coin_pub)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_RO_EXCHANGE_TO_BANK: - { - const struct TALER_EXCHANGEDB_ClosingTransfer *closing = - pos->details.closing; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - - if (TALER_EC_NONE != - TALER_exchange_online_reserve_closed_sign ( - &TEH_keys_exchange_sign_, - closing->execution_date, - &closing->amount, - &closing->closing_fee, - closing->receiver_account_details, - &closing->wtid, - &pos->details.closing->reserve_pub, - &pub, - &sig)) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "CLOSING"), - TALER_JSON_pack_full_payto ("receiver_account_details", - closing->receiver_account_details), - GNUNET_JSON_pack_data_auto ("wtid", - &closing->wtid), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_timestamp ("timestamp", - closing->execution_date), - TALER_JSON_pack_amount ("amount", - &closing->amount), - TALER_JSON_pack_amount ("closing_fee", - &closing->closing_fee)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_RO_PURSE_MERGE: - { - const struct TALER_EXCHANGEDB_PurseMerge *merge = - pos->details.merge; - - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "MERGE"), - GNUNET_JSON_pack_data_auto ("h_contract_terms", - &merge->h_contract_terms), - GNUNET_JSON_pack_data_auto ("merge_pub", - &merge->merge_pub), - GNUNET_JSON_pack_uint64 ("min_age", - merge->min_age), - GNUNET_JSON_pack_uint64 ("flags", - merge->flags), - GNUNET_JSON_pack_data_auto ("purse_pub", - &merge->purse_pub), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &merge->reserve_sig), - GNUNET_JSON_pack_timestamp ("merge_timestamp", - merge->merge_timestamp), - GNUNET_JSON_pack_timestamp ("purse_expiration", - merge->purse_expiration), - TALER_JSON_pack_amount ("purse_fee", - &merge->purse_fee), - TALER_JSON_pack_amount ("amount", - &merge->amount_with_fee), - GNUNET_JSON_pack_bool ("merged", - merge->merged)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - case TALER_EXCHANGEDB_RO_HISTORY_REQUEST: - { - const struct TALER_EXCHANGEDB_HistoryRequest *history = - pos->details.history; - - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "HISTORY"), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &history->reserve_sig), - GNUNET_JSON_pack_timestamp ("request_timestamp", - history->request_timestamp), - TALER_JSON_pack_amount ("amount", - &history->history_fee)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - - case TALER_EXCHANGEDB_RO_OPEN_REQUEST: - { - const struct TALER_EXCHANGEDB_OpenRequest *orq = - pos->details.open_request; - - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "OPEN"), - GNUNET_JSON_pack_uint64 ("requested_min_purses", - orq->purse_limit), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &orq->reserve_sig), - GNUNET_JSON_pack_timestamp ("request_timestamp", - orq->request_timestamp), - GNUNET_JSON_pack_timestamp ("requested_expiration", - orq->reserve_expiration), - TALER_JSON_pack_amount ("open_fee", - &orq->open_fee)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - - case TALER_EXCHANGEDB_RO_CLOSE_REQUEST: - { - const struct TALER_EXCHANGEDB_CloseRequest *crq = - pos->details.close_request; - - if (0 != - json_array_append_new ( - json_history, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "CLOSE"), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &crq->reserve_sig), - GNUNET_is_zero (&crq->target_account_h_payto) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("h_payto", - NULL)) - : GNUNET_JSON_pack_data_auto ("h_payto", - &crq->target_account_h_payto), - GNUNET_JSON_pack_timestamp ("request_timestamp", - crq->request_timestamp)))) - { - GNUNET_break (0); - json_decref (json_history); - return NULL; - } - } - break; - } - } - return json_history; -} - - -/** - * Add the headers we want to set for every /keys response. - * - * @param cls the key state to use - * @param[in,out] response the response to modify - */ -static void -add_response_headers (void *cls, - struct MHD_Response *response) -{ - (void) cls; - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_CACHE_CONTROL, - "no-cache")); -} - - -MHD_RESULT -TEH_handler_reserves_history ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub) -{ - struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; - uint64_t start_off = 0; - struct TALER_Amount balance; - uint64_t etag_in; - uint64_t etag_out; - char etagp[24]; - struct MHD_Response *resp; - unsigned int http_status; - - TALER_MHD_parse_request_number (rc->connection, - "start", - &start_off); - { - struct TALER_ReserveSignatureP reserve_sig; - bool required = true; - - TALER_MHD_parse_request_header_auto (rc->connection, - TALER_RESERVE_HISTORY_SIGNATURE_HEADER, - &reserve_sig, - required); - - if (GNUNET_OK != - TALER_wallet_reserve_history_verify (start_off, - reserve_pub, - &reserve_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE, - NULL); - } - } - - /* Get etag */ - { - const char *etags; - - etags = MHD_lookup_connection_value (rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if (NULL != etags) - { - char dummy; - unsigned long long ev; - - if (1 != sscanf (etags, - "\"%llu\"%c", - &ev, - &dummy)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client send malformed `If-None-Match' header `%s'\n", - etags); - etag_in = 0; - } - else - { - etag_in = (uint64_t) ev; - } - } - else - { - etag_in = start_off; - } - } - - { - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_reserve_history (TEH_plugin->cls, - reserve_pub, - start_off, - etag_in, - &etag_out, - &balance, - &rh); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_reserve_history"); - case GNUNET_DB_STATUS_SOFT_ERROR: - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "get_reserve_history"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* Handled below */ - break; - } - } - - GNUNET_snprintf (etagp, - sizeof (etagp), - "\"%llu\"", - (unsigned long long) etag_out); - if (etag_in == etag_out) - { - TEH_plugin->free_reserve_history (TEH_plugin->cls, - rh); - return TEH_RESPONSE_reply_not_modified (rc->connection, - etagp, - &add_response_headers, - NULL); - } - if (NULL == rh) - { - /* 204: empty history */ - resp = MHD_create_response_from_buffer_static (0, - ""); - http_status = MHD_HTTP_NO_CONTENT; - } - else - { - json_t *history; - - http_status = MHD_HTTP_OK; - history = compile_reserve_history (rh); - TEH_plugin->free_reserve_history (TEH_plugin->cls, - rh); - rh = NULL; - if (NULL == history) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - NULL); - } - resp = TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_amount ("balance", - &balance), - GNUNET_JSON_pack_array_steal ("history", - history)); - } - add_response_headers (NULL, - resp); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - etagp)); - { - MHD_RESULT ret; - - ret = MHD_queue_response (rc->connection, - http_status, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } -} - - -/* end of taler-exchange-httpd_reserves_history.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_history.h b/src/exchange/taler-exchange-httpd_reserves_history.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_history.h - * @brief Handle /reserves/$RESERVE_PUB/history requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RESERVES_HISTORY_H -#define TALER_EXCHANGE_HTTPD_RESERVES_HISTORY_H - -#include <microhttpd.h> -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/reserves/$RID/history" request. - * - * @param rc request context - * @param reserve_pub public key of the reserve - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reserves_history ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_open.c b/src/exchange/taler-exchange-httpd_reserves_open.c @@ -1,471 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_open.c - * @brief Handle /reserves/$RESERVE_PUB/open requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler/taler_mhd_lib.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_dbevents.h" -#include "taler-exchange-httpd_common_deposit.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_reserves_open.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * How far do we allow a client's time to be off when - * checking the request timestamp? - */ -#define TIMESTAMP_TOLERANCE \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) - - -/** - * Closure for #reserve_open_transaction. - */ -struct ReserveOpenContext -{ - /** - * Public key of the reserve the inquiry is about. - */ - const struct TALER_ReservePublicKeyP *reserve_pub; - - /** - * Desired (minimum) expiration time for the reserve. - */ - struct GNUNET_TIME_Timestamp desired_expiration; - - /** - * Actual expiration time for the reserve. - */ - struct GNUNET_TIME_Timestamp reserve_expiration; - - /** - * Timestamp of the request. - */ - struct GNUNET_TIME_Timestamp timestamp; - - /** - * Client signature approving the request. - */ - struct TALER_ReserveSignatureP reserve_sig; - - /** - * Global fees applying to the request. - */ - const struct TEH_GlobalFee *gf; - - /** - * Amount to be paid from the reserve. - */ - struct TALER_Amount reserve_payment; - - /** - * Actual cost to open the reserve. - */ - struct TALER_Amount open_cost; - - /** - * Total amount that was deposited. - */ - struct TALER_Amount total; - - /** - * Information about payments by coin. - */ - struct TEH_PurseDepositedCoin *payments; - - /** - * Length of the @e payments array. - */ - unsigned int payments_len; - - /** - * Desired minimum purse limit. - */ - uint32_t purse_limit; - - /** - * Set to true if the reserve balance is too low - * for the operation. - */ - bool no_funds; - -}; - - -/** - * Send reserve open to client. - * - * @param connection connection to the client - * @param rsc reserve open data to return - * @return MHD result code - */ -static MHD_RESULT -reply_reserve_open_success (struct MHD_Connection *connection, - const struct ReserveOpenContext *rsc) -{ - struct GNUNET_TIME_Timestamp now; - struct GNUNET_TIME_Timestamp re; - unsigned int status; - - status = MHD_HTTP_OK; - if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, - <, - rsc->desired_expiration)) - status = MHD_HTTP_PAYMENT_REQUIRED; - now = GNUNET_TIME_timestamp_get (); - if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, - <, - now)) - re = now; - else - re = rsc->reserve_expiration; - return TALER_MHD_REPLY_JSON_PACK ( - connection, - status, - GNUNET_JSON_pack_timestamp ("reserve_expiration", - re), - TALER_JSON_pack_amount ("open_cost", - &rsc->open_cost)); -} - - -/** - * Cleans up information in @a rsc, but does not - * free @a rsc itself (allocated on the stack!). - * - * @param[in] rsc struct with information to clean up - */ -static void -cleanup_rsc (struct ReserveOpenContext *rsc) -{ - for (unsigned int i = 0; i<rsc->payments_len; i++) - { - TEH_common_purse_deposit_free_coin (&rsc->payments[i]); - } - GNUNET_free (rsc->payments); -} - - -/** - * Function implementing /reserves/$RID/open transaction. Given the public - * key of a reserve, return the associated transaction open. 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 ReserveOpenContext *` - * @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 -reserve_open_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct ReserveOpenContext *rsc = cls; - enum GNUNET_DB_QueryStatus qs; - struct TALER_Amount reserve_balance; - - for (unsigned int i = 0; i<rsc->payments_len; i++) - { - struct TEH_PurseDepositedCoin *coin = &rsc->payments[i]; - bool insufficient_funds = true; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Make coin %u known\n", - i); - qs = TEH_make_coin_known (&coin->cpi, - connection, - &coin->known_coin_id, - mhd_ret); - if (qs < 0) - return qs; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Insert open deposit %u known\n", - i); - qs = TEH_plugin->insert_reserve_open_deposit ( - TEH_plugin->cls, - &coin->cpi, - &coin->coin_sig, - coin->known_coin_id, - &coin->amount, - &rsc->reserve_sig, - rsc->reserve_pub, - &insufficient_funds); - /* 0 == qs is fine, then the coin was already - spent for this very operation as identified - by reserve_sig! */ - 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_reserve_open_deposit"); - return qs; - } - if (insufficient_funds) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handle insufficient funds\n"); - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &coin->cpi.denom_pub_hash, - &coin->cpi.coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Do reserve open with reserve payment of %s\n", - TALER_amount2s (&rsc->total)); - qs = TEH_plugin->do_reserve_open (TEH_plugin->cls, - /* inputs */ - rsc->reserve_pub, - &rsc->total, - &rsc->reserve_payment, - rsc->purse_limit, - &rsc->reserve_sig, - rsc->desired_expiration, - rsc->timestamp, - &rsc->gf->fees.account, - /* outputs */ - &rsc->no_funds, - &reserve_balance, - &rsc->open_cost, - &rsc->reserve_expiration); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - *mhd_ret - = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_reserve_open"); - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - *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; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - if (rsc->no_funds) - { - TEH_plugin->rollback (TEH_plugin->cls); - *mhd_ret - = TEH_RESPONSE_reply_reserve_insufficient_balance ( - connection, - TALER_EC_EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS, - &reserve_balance, - &rsc->reserve_payment, - rsc->reserve_pub); - return GNUNET_DB_STATUS_HARD_ERROR; - } - return qs; -} - - -MHD_RESULT -TEH_handler_reserves_open (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - struct ReserveOpenContext rsc; - const json_t *payments; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_timestamp ("request_timestamp", - &rsc.timestamp), - GNUNET_JSON_spec_timestamp ("reserve_expiration", - &rsc.desired_expiration), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &rsc.reserve_sig), - GNUNET_JSON_spec_uint32 ("purse_limit", - &rsc.purse_limit), - GNUNET_JSON_spec_array_const ("payments", - &payments), - TALER_JSON_spec_amount ("reserve_payment", - TEH_currency, - &rsc.reserve_payment), - GNUNET_JSON_spec_end () - }; - - rsc.reserve_pub = reserve_pub; - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (rc->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 */ - } - } - - { - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); - if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, - rsc.timestamp.abs_time, - TIMESTAMP_TOLERANCE)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, - NULL); - } - } - - rsc.payments_len = json_array_size (payments); - rsc.payments = GNUNET_new_array (rsc.payments_len, - struct TEH_PurseDepositedCoin); - rsc.total = rsc.reserve_payment; - for (unsigned int i = 0; i<rsc.payments_len; i++) - { - struct TEH_PurseDepositedCoin *coin = &rsc.payments[i]; - enum GNUNET_GenericReturnValue res; - - res = TEH_common_purse_deposit_parse_coin ( - rc->connection, - coin, - json_array_get (payments, - i)); - if (GNUNET_SYSERR == res) - { - GNUNET_break (0); - cleanup_rsc (&rsc); - return MHD_NO; /* hard failure */ - } - if (GNUNET_NO == res) - { - GNUNET_break_op (0); - cleanup_rsc (&rsc); - return MHD_YES; /* failure */ - } - if (0 > - TALER_amount_add (&rsc.total, - &rsc.total, - &coin->amount_minus_fee)) - { - GNUNET_break (0); - cleanup_rsc (&rsc); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - NULL); - } - } - - { - struct TEH_KeyStateHandle *keys; - - keys = TEH_keys_get_state (); - if (NULL == keys) - { - GNUNET_break (0); - cleanup_rsc (&rsc); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - } - rsc.gf = TEH_keys_global_fee_by_time (keys, - rsc.timestamp); - } - if (NULL == rsc.gf) - { - GNUNET_break (0); - cleanup_rsc (&rsc); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, - NULL); - } - - if (GNUNET_OK != - TALER_wallet_reserve_open_verify (&rsc.reserve_payment, - rsc.timestamp, - rsc.desired_expiration, - rsc.purse_limit, - reserve_pub, - &rsc.reserve_sig)) - { - GNUNET_break_op (0); - cleanup_rsc (&rsc); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE, - NULL); - } - - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "reserve open", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &reserve_open_transaction, - &rsc)) - { - cleanup_rsc (&rsc); - return mhd_ret; - } - } - - { - MHD_RESULT mhd_ret; - - mhd_ret = reply_reserve_open_success (rc->connection, - &rsc); - cleanup_rsc (&rsc); - return mhd_ret; - } -} - - -/* end of taler-exchange-httpd_reserves_open.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_open.h b/src/exchange/taler-exchange-httpd_reserves_open.h @@ -1,41 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_open.h - * @brief Handle /reserves/$RESERVE_PUB/open requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RESERVES_OPEN_H -#define TALER_EXCHANGE_HTTPD_RESERVES_OPEN_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a POST "/reserves/$RID/open" request. - * - * @param rc request context - * @param reserve_pub public key of the reserve - * @param root uploaded body from the client - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reserves_open (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reserves_purse.c b/src/exchange/taler-exchange-httpd_reserves_purse.c @@ -1,894 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022-2024 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 <http://www.gnu.org/licenses/> -*/ -/** - * @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 "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_reserves_purse.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_exchangedb_lib.h" -#include "taler-exchange-httpd_keys.h" - - -/** - * Closure for #purse_transaction. - */ -struct ReservePurseContext -{ - - /** - * Kept in a DLL. - */ - struct ReservePurseContext *next; - - /** - * Kept in a DLL. - */ - struct ReservePurseContext *prev; - - /** - * Our request context. - */ - struct TEH_RequestContext *rc; - - /** - * Handle for legitimization check. - */ - struct TEH_LegitimizationCheckHandle *lch; - - /** - * Response to return. Note that the response must - * be queued or destroyed by the callee. NULL - * if the legitimization check was successful and the handler should return - * a handler-specific result. - */ - struct MHD_Response *response; - - /** - * Payto URI for the reserve. - */ - struct TALER_NormalizedPayto payto_uri; - - /** - * Public key of the account (reserve) we are creating a purse for. - */ - union TALER_AccountPublicKeyP account_pub; - - /** - * 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_NormalizedPaytoHashP 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; - - /** - * HTTP status code for @a response, or 0 - */ - unsigned int http_status; - - /** - * Do we lack an @e econtract? - */ - bool no_econtract; - - /** - * Set to true if the purse_fee was not given in the REST request. - */ - bool no_purse_fee; - -}; - -/** - * Kept in a DLL. - */ -static struct ReservePurseContext *rpc_head; - -/** - * Kept in a DLL. - */ -static struct ReservePurseContext *rpc_tail; - - -void -TEH_reserves_purse_cleanup () -{ - struct ReservePurseContext *rpc; - - while (NULL != (rpc = rpc_head)) - { - GNUNET_CONTAINER_DLL_remove (rpc_head, - rpc_tail, - rpc); - MHD_resume_connection (rpc->rc->connection); - } -} - - -/** - * 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 - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -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; - enum GNUNET_GenericReturnValue ret; - - ret = cb (cb_cls, - &rpc->pd.target_amount, - GNUNET_TIME_absolute_get ()); - GNUNET_break (GNUNET_SYSERR != ret); - if (GNUNET_OK != ret) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - 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); - return qs; -} - - -/** - * Function called with the result of a legitimization - * check. - * - * @param cls closure - * @param lcr legitimization check result - */ -static void -reserve_purse_legi_cb ( - void *cls, - const struct TEH_LegitimizationCheckResult *lcr) -{ - struct ReservePurseContext *rpc = cls; - - rpc->lch = NULL; - rpc->http_status = lcr->http_status; - rpc->response = lcr->response; - rpc->kyc = lcr->kyc; - GNUNET_CONTAINER_DLL_remove (rpc_head, - rpc_tail, - rpc); - MHD_resume_connection (rpc->rc->connection); - TALER_MHD_daemon_trigger (); -} - - -/** - * 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; - - { - 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->purse_fee, - &rpc->account_pub.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->purse_fee)) ) ) - { - *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)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - return qs; -} - - -/** - * Function to clean up our rh_ctx in @a rc - * - * @param[in,out] rc context to clean up - */ -static void -rpc_cleaner (struct TEH_RequestContext *rc) -{ - struct ReservePurseContext *rpc = rc->rh_ctx; - - if (NULL != rpc->lch) - { - TEH_legitimization_check_cancel (rpc->lch); - rpc->lch = NULL; - } - GNUNET_free (rpc->econtract.econtract); - GNUNET_free (rpc->payto_uri.normalized_payto); - GNUNET_free (rpc); -} - - -MHD_RESULT -TEH_handler_reserves_purse ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - struct ReservePurseContext *rpc = rc->rh_ctx; - struct MHD_Connection *connection = rc->connection; - - if (NULL == rpc) - { - rpc = GNUNET_new (struct ReservePurseContext); - rc->rh_ctx = rpc; - rc->rh_cleaner = &rpc_cleaner; - rpc->rc = rc; - rpc->account_pub.reserve_pub = *reserve_pub; - rpc->exchange_timestamp - = GNUNET_TIME_timestamp_get (); - rpc->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), - &rpc->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 */ - } - } - } - - rpc->payto_uri - = TALER_reserve_make_payto (TEH_base_url, - &rpc->account_pub.reserve_pub); - TALER_normalized_payto_hash (rpc->payto_uri, - &rpc->h_payto); - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - - - if (GNUNET_OK != - TALER_wallet_purse_merge_verify (rpc->payto_uri, - rpc->merge_timestamp, - &rpc->pd.purse_pub, - &rpc->merge_pub, - &rpc->merge_sig)) - { - MHD_RESULT ret; - - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID, - rpc->payto_uri.normalized_payto); - return ret; - } - 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); - 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); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER, - NULL); - } - - { - struct TEH_KeyStateHandle *keys; - const struct TEH_GlobalFee *gf; - - keys = TEH_keys_get_state (); - if (NULL == keys) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - } - gf = TEH_keys_global_fee_by_time (keys, - rpc->exchange_timestamp); - if (NULL == gf) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create purse: global fees not configured!\n"); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING, - NULL); - } - if (rpc->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, - &gf->fees.purse)) - { - /* rpc->purse_fee is below gf.fees.purse! */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW, - TALER_amount2s (&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); - 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->account_pub.reserve_pub, - &rpc->reserve_sig)) - { - GNUNET_break_op (0); - 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"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID, - NULL); - } - { - struct TALER_FullPayto fake_full_payto; - - GNUNET_asprintf (&fake_full_payto.full_payto, - "%s?receiver-name=wallet", - rpc->payto_uri.normalized_payto); - rpc->lch = TEH_legitimization_check ( - &rpc->rc->async_scope_id, - TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, - fake_full_payto, - &rpc->h_payto, - &rpc->account_pub, - &amount_iterator, - rpc, - &reserve_purse_legi_cb, - rpc); - GNUNET_free (fake_full_payto.full_payto); - } - GNUNET_assert (NULL != rpc->lch); - MHD_suspend_connection (rc->connection); - GNUNET_CONTAINER_DLL_insert (rpc_head, - rpc_tail, - rpc); - return MHD_YES; - } - if (NULL != rpc->response) - return MHD_queue_response (connection, - rpc->http_status, - rpc->response); - if (! rpc->kyc.ok) - { - if (0 == rpc->kyc.requirement_row) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "requirement row not set"); - } - return TEH_RESPONSE_reply_kyc_required ( - connection, - &rpc->h_payto, - &rpc->kyc, - false); - } - - { - MHD_RESULT mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "execute reserve purse", - TEH_MT_REQUEST_RESERVE_PURSE, - &mhd_ret, - &purse_transaction, - rpc)) - { - return mhd_ret; - } - } - - /* generate regular response */ - { - MHD_RESULT res; - - res = TEH_RESPONSE_reply_purse_created ( - connection, - rpc->exchange_timestamp, - &rpc->deposit_total, - &rpc->pd); - return res; - } -} - - -/* end of taler-exchange-httpd_reserves_purse.c */ diff --git a/src/exchange/taler-exchange-httpd_reserves_purse.h b/src/exchange/taler-exchange-httpd_reserves_purse.h @@ -1,53 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reserves_purse.h - * @brief Handle /reserves/$RID/purse requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_RESERVES_PURSE_H -#define TALER_EXCHANGE_HTTPD_RESERVES_PURSE_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Resume suspended connections, we are shutting down. - */ -void -TEH_reserves_purse_cleanup (void); - - -/** - * Handle a "/reserves/$RESERVE_PUB/purse" request. Parses the JSON, and, if - * successful, passes the JSON data to #create_transaction() to further check - * the details of the operation specified. If everything checks out, this - * will ultimately lead to the "purses create" being executed, or rejected. - * - * @param rc request context - * @param reserve_pub public key of the reserve - * @param root uploaded JSON data - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reserves_purse ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); - -#endif diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c @@ -31,7 +31,7 @@ #include "taler/taler_util.h" #include "taler/taler_json_lib.h" #include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_get-keys.h" MHD_RESULT diff --git a/src/exchange/taler-exchange-httpd_reveal-melt.c b/src/exchange/taler-exchange-httpd_reveal-melt.c @@ -1,877 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023,2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reveal-melt.c - * @brief Handle /reveal-melt requests - * @author Özgür Kesim - */ -#include "taler/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/taler_error_codes.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_mhd.h" -#include "taler-exchange-httpd_reveal-melt.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - -#define KAPPA_MINUS_1 (TALER_CNC_KAPPA - 1) - - -/** - * State for an /reveal-melt operation. - */ -struct MeltRevealContext -{ - - /** - * Commitment for the melt operation, previously called by the - * client. - */ - struct TALER_RefreshCommitmentP rc; - - /** - * The data from the original melt. Will be retrieved from - * the DB via @a rc. - */ - struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh; - - /** - * True, if @e signatures were not provided in the request. - */ - bool no_signatures; - - /** - * @since v27 - * @deprecated after vDOLDPLUS - * - * TALER_CNC_KAPPA-1 disclosed signatures for public refresh nonces. - */ - struct TALER_PrivateRefreshNonceSignatureP signatures[KAPPA_MINUS_1]; - - /** - * True, if @e transfer_secret_seeds were not provided in the request. - */ - bool no_transfer_secret_seeds; - - /** - * @since vDOLDPLUS - * - * The transfer secret seeds for the revealed batches of coins. - */ - struct TALER_PrivateRefreshBatchSeedP transfer_secret_seeds[KAPPA_MINUS_1]; - - /** - * True, if no @e age_commitment was provided - */ - bool no_age_commitment; - - /** - * If @e no_age_commitment is false, the age commitment of - * the old coin. Needed to ensure that the age commitment - * is applied correctly to the fresh coins. - */ - struct TALER_AgeCommitment age_commitment; -}; - - -/** - * Check if the request belongs to an existing refresh request. - * If so, sets the refresh object with the request data. - * Otherwise, it queues an appropriate MHD response. - * - * @param connection The HTTP connection to the client - * @param rc Original commitment value sent with the melt request - * @param[out] refresh Data from the original refresh request - * @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 refresh request has been found, - * #GNUNET_SYSERR if we did not find the request in the DB - */ -static enum GNUNET_GenericReturnValue -find_original_refresh ( - struct MHD_Connection *connection, - const struct TALER_RefreshCommitmentP *rc, - struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh, - MHD_RESULT *result) -{ - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int retry = 0; retry < 3; retry++) - { - qs = TEH_plugin->get_refresh (TEH_plugin->cls, - rc, - refresh); - 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_REFRESHES_REVEAL_SESSION_UNKNOWN, - NULL); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_HARD_ERROR: - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_refresh"); - 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 unsuccessful retries*/ - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_refresh"); - return GNUNET_SYSERR; -} - - -/** - * Verify that the age commitment is sound, that is, if the - * previous /melt provided a hash, ensure we have the corresponding - * age commitment. Or not, if it wasn't provided. - * - * @param connection The MHD connection to handle - * @param actx The context of the operation, only partially built at this time - * @param[out] mhd_ret The result if a reply is queued for MHD - * @return #GNUNET_OK on success, otherwise a reply is queued for MHD and @a mhd_ret is set - */ -static enum GNUNET_GenericReturnValue -compare_age_commitment ( - struct MHD_Connection *connection, - struct MeltRevealContext *actx, - MHD_RESULT *mhd_ret) -{ - if (actx->no_age_commitment != - actx->refresh.coin.no_age_commitment) - { - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, - NULL); - return GNUNET_SYSERR; - } - if (! actx->no_age_commitment) - { - struct TALER_AgeCommitmentHashP ach; - - actx->age_commitment.mask = TEH_age_restriction_config.mask; - TALER_age_commitment_hash ( - &actx->age_commitment, - &ach); - if (0 != GNUNET_memcmp ( - &actx->refresh.coin.h_age_commitment, - &ach)) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, - NULL); - return GNUNET_SYSERR; - } - } - return GNUNET_OK; -} - - -/** - * @brief Derives an planchet from a given input and returns - * blinded planchets detail - * - * @param connection Connection to the client - * @param denom_key The denomination key - * @param secret The secret to a planchet - * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL - * @param nonce The derived nonce needed for CS denomination - * @param old_age_commitment The age commitment of the old coin, might be NULL - * @param[out] detail planchet detail to write 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 enum GNUNET_GenericReturnValue -calculate_blinded_detail ( - struct MHD_Connection *connection, - struct TEH_DenominationKey *denom_key, - const struct TALER_PlanchetMasterSecretP *secret, - const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub, - union GNUNET_CRYPTO_BlindSessionNonce *nonce, - const struct TALER_AgeCommitment *old_age_commitment, - struct TALER_PlanchetDetail *detail, - MHD_RESULT *result) -{ - enum GNUNET_GenericReturnValue ret; - struct TALER_AgeCommitmentHashP ach; - bool no_age_commitment = (NULL == old_age_commitment); - - /* calculate age commitment hash */ - if (! no_age_commitment) - { - struct TALER_AgeCommitment nac; - - TALER_age_commitment_derive_from_secret (old_age_commitment, - secret, - &nac); - TALER_age_commitment_hash (&nac, - &ach); - TALER_age_commitment_free (&nac); - } - - /* Next: calculate planchet */ - { - struct TALER_CoinPubHashP c_hash; - struct TALER_CoinSpendPrivateKeyP coin_priv; - union GNUNET_CRYPTO_BlindingSecretP bks; - struct GNUNET_CRYPTO_BlindingInputValues bi = { - .cipher = denom_key->denom_pub.bsign_pub_key->cipher - }; - struct TALER_ExchangeBlindingValues blinding_values = { - .blinding_inputs = &bi - }; - - switch (bi.cipher) - { - case GNUNET_CRYPTO_BSA_CS: - GNUNET_assert (NULL != r_pub); - GNUNET_assert (NULL != nonce); - bi.details.cs_values = *r_pub; - break; - case GNUNET_CRYPTO_BSA_RSA: - break; - default: - GNUNET_assert (0); - } - - TALER_planchet_blinding_secret_create (secret, - &blinding_values, - &bks); - TALER_planchet_setup_coin_priv (secret, - &blinding_values, - &coin_priv); - ret = TALER_planchet_prepare (&denom_key->denom_pub, - &blinding_values, - &bks, - nonce, - &coin_priv, - no_age_commitment - ? NULL - : &ach, - &c_hash, - detail); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - *result = TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - GNUNET_JSON_pack_string ( - "details", - "failed to prepare planchet from base key")); - return ret; - } - } - return ret; -} - - -/** - * @brief Checks the validity of the disclosed signatures as follows: - * - Verifies the validity of the disclosed signatures with the old coin's public key - * - Derives the seeds for disclosed fresh coins - * - Derives the fresh coins from the seeds - * - Derives new age commitment - * - Calculates the blinded coin planchet hashes - * - Calculates the refresh commitment from above data - * - Compares the calculated commitment with existing one - * - * The derivation of a fresh coin from the old coin is defined in - * https://docs.taler.net/design-documents/062-pq-refresh.html - * - * The derivation of age-commitment from a coin's age-commitment - * https://docs.taler.net/design-documents/024-age-restriction.html#melt - * - * @param con HTTP-connection to the client - * @param rf Original refresh object from the previous /melt request - * @param old_age_commitment The age commitment of the original coin - * @param signatures The secrets of the disclosed coins, KAPPA_MINUS_1*num_coins many, maybe NULL - * @param rev_batch_seeds The seeds for the transfer secrets of the disclosed coins, KAPPA_MINUS_1 many, maybe NULL - * @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 ( - struct MHD_Connection *con, - const struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *rf, - const struct TALER_AgeCommitment *old_age_commitment, - const struct TALER_PrivateRefreshNonceSignatureP (*signatures)[KAPPA_MINUS_1], - const struct TALER_PrivateRefreshBatchSeedP (*rev_batch_seeds)[KAPPA_MINUS_1], - MHD_RESULT *result) -{ - enum GNUNET_GenericReturnValue ret; - struct TEH_KeyStateHandle *keys; - struct TEH_DenominationKey *denom_keys[rf->num_coins]; - struct TALER_DenominationHashP *denoms_h[rf->num_coins]; - struct TALER_Amount total_amount; - struct TALER_Amount total_fee; - bool is_cs[rf->num_coins]; - size_t cs_count = 0; - - GNUNET_assert (rf->noreveal_index < TALER_CNC_KAPPA); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &total_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &total_fee)); - memset (denom_keys, - 0, - sizeof(denom_keys)); - memset (is_cs, - 0, - sizeof(is_cs)); - - /** - * Consistency check: - * If the refresh was for v27, signatures must not be NULL, - * otherwise the revealed batch seeds must not be NULL. - */ - if (rf->is_v27_refresh) - { - if (NULL == signatures) - { - *result = TALER_MHD_reply_with_error (con, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "signatures missing"); - return GNUNET_SYSERR; - } - } - else /* vDOLDPLUS */ - { - if (NULL == rev_batch_seeds) - { - *result = TALER_MHD_reply_with_error (con, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "batch_seeds missing"); - 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 (con, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - - /** - * Find the denomination keys from the original request to /melt - * and keep track of those of type CS. - */ - for (size_t i = 0; i < rf->num_coins; i++) - { - denom_keys[i] = - TEH_keys_denomination_by_serial_from_state ( - keys, - rf->denom_serials[i]); - if (NULL == denom_keys[i]) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - - /* Accumulate amount and fees */ - GNUNET_assert (0 <= TALER_amount_add (&total_amount, - &total_amount, - &denom_keys[i]->meta.value)); - GNUNET_assert (0 <= TALER_amount_add (&total_fee, - &total_fee, - &denom_keys[i]->meta.fees.refresh)); - - if (GNUNET_CRYPTO_BSA_CS == - denom_keys[i]->denom_pub.bsign_pub_key->cipher) - { - is_cs[i] = true; - cs_count++; - } - - /* Remember the hash of the public key of the denomination for later */ - denoms_h[i] = &denom_keys[i]->h_denom_pub; - } - - /** - * Sanity check: - * The number CS denominations must match those from the /melt request - */ - if (cs_count != rf->num_cs_r_values) - { - GNUNET_break (0); - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - return GNUNET_SYSERR; - } - /** - * First things first for v27 clients: Verify the signature of the old coin - * over the refresh nonce. This proves the ownership - * for the fresh coin. - */ - if (rf->is_v27_refresh) - { - size_t sig_idx = 0; - struct TALER_KappaPublicRefreshNoncesP kappa_nonces; - - GNUNET_assert (NULL != signatures); - - /** - * We expand the provided refresh_seed from the original call to /melt, - * into kappa many batch seeds, from which we will later use all except the - * noreveal_index one. - */ - TALER_refresh_expand_kappa_nonces_v27 ( - &rf->refresh_seed, - &kappa_nonces); - - for (uint8_t k=0; k < TALER_CNC_KAPPA; k++) - { - if (rf->noreveal_index == k) - continue; - if (GNUNET_OK != - TALER_wallet_refresh_nonce_verify ( - &rf->coin.coin_pub, - &kappa_nonces.tuple[k], - rf->num_coins, - denoms_h, - k, - &(*signatures)[sig_idx++])) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID, - NULL); - return GNUNET_SYSERR; - } - } - } - /** - * In the following scope, we start collecting blinded coin planchet hashes, - * either those persisted from the original request to /melt, or we - * derive and calculate them from the provided signatures, after having - * verified that each of them was signed by the old coin's private key. - * - * After collecting the blinded coin planchet hashes, we can then get - * the commitment for the calculated values and compare the result with - * the commitment from the /melt request. - */ - { - struct TALER_KappaHashBlindedPlanchetsP kappa_planchets_h; - union GNUNET_CRYPTO_BlindSessionNonce b_nonces[GNUNET_NZL (cs_count)]; - size_t cs_idx = 0; /* [0...cs_count) */ - uint8_t sig_idx = 0; /* [0..KAPPA_MINUS_1) */ - /* These two are only necessary for non-v27 clients, but this is the common case. */ - struct TALER_TransferPublicKeyP k_tpbs[TALER_CNC_KAPPA][rf->num_coins]; - struct TALER_KappaTransferPublicKeys kappa_transfer_pubs = { - .num_transfer_pubs = rf->num_coins - }; - /** - * First, derive the blinding nonces for the CS denominations all at once. - */ - if (0 < cs_count) - { - uint32_t cs_indices[cs_count]; - size_t idx = 0; /* [0...cs_count) */ - - for (size_t i = 0; i < rf->num_coins; i++) - if (is_cs[i]) - cs_indices[idx++] = i; - - TALER_cs_derive_only_cs_blind_nonces_from_seed (&rf->blinding_seed, - true, /* for melt */ - cs_count, - cs_indices, - b_nonces); - } - /** - * We handle the kappa batches of rf->num_coins depths first. - */ - for (uint8_t k = 0; k<TALER_CNC_KAPPA; k++) - { - if (k == rf->noreveal_index) - { - /** - * We take the stored value for the hash of selected batch - */ - kappa_planchets_h.tuple[k] = rf->selected_h; - } - else - { - /** - * We have to generate all the planchets' details from - * the disclosed input material and generate the - * hashes of them. - */ - struct TALER_PlanchetMasterSecretP planchet_secrets[rf->num_coins]; - struct TALER_PlanchetDetail details[rf->num_coins]; - - memset (planchet_secrets, - 0, - sizeof(planchet_secrets)); - memset (details, - 0, - sizeof(details)); - - GNUNET_assert (sig_idx < KAPPA_MINUS_1); - - /** - * Expand from the k-th signature all num_coin planchet secrets, - * except for the noreveal_index. - */ - if (rf->is_v27_refresh) - { - TALER_refresh_signature_to_secrets_v27 ( - &(*signatures)[sig_idx++], - rf->num_coins, - planchet_secrets); - } - else /* vDOLDPLUS */ - { - TALER_refresh_expand_batch_seed_to_transfer_data ( - &(*rev_batch_seeds)[sig_idx++], - &rf->coin.coin_pub, - rf->num_coins, - planchet_secrets, - k_tpbs[k]); - - kappa_transfer_pubs.batch[k] = k_tpbs[k]; - } - /** - * Reset the index for the CS denominations. - */ - cs_idx = 0; - - for (size_t coin_idx = 0; coin_idx < rf->num_coins; coin_idx++) - { - struct GNUNET_CRYPTO_CSPublicRPairP *rp; - union GNUNET_CRYPTO_BlindSessionNonce *np; - - if (is_cs[coin_idx]) - { - GNUNET_assert (cs_idx < cs_count); - np = &b_nonces[cs_idx]; - rp = &rf->cs_r_values[cs_idx]; - cs_idx++; - } - else - { - np = NULL; - rp = NULL; - } - ret = calculate_blinded_detail (con, - denom_keys[coin_idx], - &planchet_secrets[coin_idx], - rp, - np, - old_age_commitment, - &details[coin_idx], - result); - if (GNUNET_OK != ret) - return GNUNET_SYSERR; - } - /** - * Now we can generate the hashes for the kappa-th batch of coins - */ - TALER_wallet_blinded_planchet_details_hash ( - rf->num_coins, - details, - &kappa_planchets_h.tuple[k]); - - for (size_t i =0; i<rf->num_coins; i++) - TALER_planchet_detail_free (&details[i]); - } - } - /** - * Finally, calculate the refresh commitment and compare it with the original. - */ - { - struct TALER_RefreshCommitmentP rc; - - if (rf->is_v27_refresh) - { - TALER_refresh_get_commitment_v27 (&rc, - &rf->refresh_seed, - rf->no_blinding_seed - ? NULL - : &rf->blinding_seed, - &kappa_planchets_h, - &rf->coin.coin_pub, - &rf->amount_with_fee); - } - else - { - TALER_refresh_get_commitment (&rc, - &rf->refresh_seed, - rf->no_blinding_seed - ? NULL - : &rf->blinding_seed, - &kappa_transfer_pubs, - &kappa_planchets_h, - &rf->coin.coin_pub, - &rf->amount_with_fee); - } - - if (0 != GNUNET_CRYPTO_hash_cmp ( - &rf->rc.session_hash, - &rc.session_hash)) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_INVALID_RCH, - "rc"); - return GNUNET_SYSERR; - } - } - } - return GNUNET_OK; -} - - -/** - * @brief Commit the successful reveal to the database - * - * @param con HTTP-connection to the client - * @param rc Original refresh commitment from the previous /melt 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 -commit_reveal ( - struct MHD_Connection *con, - const struct TALER_RefreshCommitmentP *rc, - MHD_RESULT *result) -{ - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int retry = 0; retry < 3; retry++) - { - qs = TEH_plugin->mark_refresh_reveal_success ( - TEH_plugin->cls, - rc); - 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 (con, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN, - NULL); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_HARD_ERROR: - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_GENERIC_DB_STORE_FAILED, - "mark_refresh_reveal_success"); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SOFT_ERROR: - break; /* try again */ - default: - GNUNET_break (0); - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - return GNUNET_SYSERR; - } - } - /* after unsuccessful retries*/ - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_GENERIC_DB_STORE_FAILED, - "mark_refresh_reveal_success"); - return GNUNET_SYSERR; - -} - - -/** - * @brief Send a response for "/reveal-melt" - * - * @param connection The http connection to the client to send the response to - * @param refresh The data from the previous call to /melt with signatures - * @return a MHD result code - */ -static MHD_RESULT -reply_melt_reveal_success ( - struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh) -{ - json_t *list = json_array (); - GNUNET_assert (NULL != list); - - for (unsigned int i = 0; i < refresh->num_coins; i++) - { - json_t *obj = GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_denom_sig (NULL, - &refresh->denom_sigs[i])); - GNUNET_assert (0 == - json_array_append_new (list, - obj)); - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("ev_sigs", - list)); -} - - -MHD_RESULT -TEH_handler_reveal_melt ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[2]) -{ - MHD_RESULT result = MHD_NO; - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - struct MeltRevealContext actx = {0}; - struct GNUNET_JSON_Specification sig_tuple[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &actx.signatures[0]), - GNUNET_JSON_spec_fixed_auto (NULL, - &actx.signatures[1]), - GNUNET_JSON_spec_end () - }; - struct GNUNET_JSON_Specification seeds_tuple[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &actx.transfer_secret_seeds[0]), - GNUNET_JSON_spec_fixed_auto (NULL, - &actx.transfer_secret_seeds[1]), - GNUNET_JSON_spec_end () - }; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("rc", - &actx.rc), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_tuple_of ("signatures", - sig_tuple), - &actx.no_signatures), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_tuple_of ("batch_seeds", - seeds_tuple), - &actx.no_transfer_secret_seeds), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_age_commitment ("age_commitment", - &actx.age_commitment), - &actx.no_age_commitment), - GNUNET_JSON_spec_end () - }; - - /** - * Note that above, we have hard-wired - * the size of TALER_CNC_KAPPA. - * Let's make sure we keep this in sync. - */ - _Static_assert (KAPPA_MINUS_1 == 2, - "TALER_CNC_KAPPA isn't 3!?!?"); - - /* Parse JSON body*/ - ret = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; - } - - (void) args; - - do { - /* Find original commitment */ - if (GNUNET_OK != - find_original_refresh ( - rc->connection, - &actx.rc, - &actx.refresh, - &result)) - break; - - /* Compare age commitment with the hash from the /melt request, if present */ - if (GNUNET_OK != - compare_age_commitment ( - rc->connection, - &actx, - &result)) - break; - - /* verify the commitment */ - if (GNUNET_OK != - verify_commitment ( - rc->connection, - &actx.refresh, - actx.no_age_commitment - ? NULL - : &actx.age_commitment, - actx.no_signatures - ? NULL - : &actx.signatures, - actx.no_transfer_secret_seeds - ? NULL - : &actx.transfer_secret_seeds, - &result)) - break; - - if (GNUNET_OK != - commit_reveal (rc->connection, - &actx.rc, - &result)) - break; - - /* Finally, return the signatures */ - result = reply_melt_reveal_success (rc->connection, - &actx.refresh); - - } while (0); - - GNUNET_JSON_parse_free (spec); - if (NULL != actx.refresh.denom_sigs) - for (unsigned int i = 0; i<actx.refresh.num_coins; i++) - TALER_blinded_denom_sig_free (&actx.refresh.denom_sigs[i]); - GNUNET_free (actx.refresh.denom_sigs); - GNUNET_free (actx.refresh.denom_pub_hashes); - GNUNET_free (actx.refresh.denom_serials); - GNUNET_free (actx.refresh.cs_r_values); - return result; -} - - -/* end of taler-exchange-httpd_reveal_melt.c */ diff --git a/src/exchange/taler-exchange-httpd_reveal-melt.h b/src/exchange/taler-exchange-httpd_reveal-melt.h @@ -1,58 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reveal-melt.h - * @brief Handle /reveal-melt requests - * @author Özgür Kesim - */ -#ifndef TALER_EXCHANGE_HTTPD_REVEAL_MELT_H -#define TALER_EXCHANGE_HTTPD_REVEAL_MELT_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/reveal-melt" request. - * - * The client got a noreveal_index in response to a previous request - * /melt. It now has to reveal all n*(kappa-1) signatures (except for the - * noreveal_index), signed by the old coin private key, over the coin's - * specific nonce, which is derived per coin from the refresh_seed, - * From that signature also all other coin-relevant data (blinding, age - * restriction, nonce) are derived from. - * - * The exchange computes those values, potentially ensures that age restriction - * is correctly applied, calculates the hash of the blinded envelopes, and - - * together with the non-disclosed blinded envelopes - compares the hash of the - * calculated melt commitment against the original. - * - * If all those checks and the used denominations turn out to be correct, the - * exchange signs all blinded envelopes with their appropriate denomination - * keys. - * - * @param rc request context - * @param root uploaded JSON data - * @param args not used - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reveal_melt ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[2]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_reveal-withdraw.c b/src/exchange/taler-exchange-httpd_reveal-withdraw.c @@ -1,637 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023,2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reveal-withdraw.c - * @brief Handle /reveal-withdraw requests - * @author Özgür Kesim - */ -#include "taler/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/taler_error_codes.h" -#include "taler/taler_exchangedb_plugin.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_mhd.h" -#include "taler-exchange-httpd_reveal-withdraw.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - -/** - * State for an /reveal-withdraw operation. - */ -struct WithdrawRevealContext -{ - - /** - * Commitment for the withdraw operation, previously called by the - * client. - */ - struct TALER_HashBlindedPlanchetsP planchets_h; - - /** - * TALER_CNC_KAPPA-1 secrets for disclosed coin batches. - */ - struct TALER_RevealWithdrawMasterSeedsP disclosed_batch_seeds; - - /** - * The data from the original withdraw. Will be retrieved from - * the DB via @a wch. - */ - struct TALER_EXCHANGEDB_Withdraw withdraw; -}; - - -/** - * Parse the json body of an '/reveal-withdraw' request. It extracts - * the denomination hashes, blinded coins and disclosed coins and allocates - * memory for those. - * - * @param connection The MHD connection to handle - * @param j_disclosed_batch_seeds The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from - * @param[out] actx 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_withdraw_reveal_json ( - struct MHD_Connection *connection, - const json_t *j_disclosed_batch_seeds, - struct WithdrawRevealContext *actx, - MHD_RESULT *mhd_ret) -{ - size_t num_entries; - const char *error; - struct GNUNET_JSON_Specification tuple[] = { - GNUNET_JSON_spec_fixed (NULL, - &actx->disclosed_batch_seeds.tuple[0], - sizeof(actx->disclosed_batch_seeds.tuple[0])), - GNUNET_JSON_spec_fixed (NULL, - &actx->disclosed_batch_seeds.tuple[1], - sizeof(actx->disclosed_batch_seeds.tuple[1])), - GNUNET_JSON_spec_end () - }; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_tuple_of (NULL, - tuple), - GNUNET_JSON_spec_end () - }; - - /** - * Note that above, in tuple[], we have hard-wired - * the size of TALER_CNC_KAPPA. - * Let's make sure we keep this in sync. - */ - _Static_assert ((TALER_CNC_KAPPA - 1) == 2); - - num_entries = json_array_size (j_disclosed_batch_seeds); /* 0, if not an array */ - if (! json_is_array (j_disclosed_batch_seeds)) - error = "disclosed_batch_seeds must be an array"; - else if (num_entries == 0) - error = "disclosed_batch_seeds must not be empty"; - else if (num_entries != TALER_CNC_KAPPA - 1) - error = - "disclosed_batch_seeds must be an array of size " - TALER_CNC_KAPPA_MINUS_ONE_STR; - else - error = NULL; - - if ( (NULL != error) || - (GNUNET_OK != - GNUNET_JSON_parse (j_disclosed_batch_seeds, - spec, - &error, - NULL)) ) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - error); - return GNUNET_SYSERR; - } - - return GNUNET_OK; -} - - -/** - * Check if the request belongs to an existing withdraw request. - * If so, sets the withdraw object with the request data. - * Otherwise, it queues an appropriate MHD response. - * - * @param connection The HTTP connection to the client - * @param planchets_h Original commitment value sent with the withdraw request - * @param[out] withdraw Data from the original withdraw request - * @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 withdraw request has been found, - * #GNUNET_SYSERR if we did not find the request in the DB - */ -static enum GNUNET_GenericReturnValue -find_original_withdraw ( - struct MHD_Connection *connection, - const struct TALER_HashBlindedPlanchetsP *planchets_h, - struct TALER_EXCHANGEDB_Withdraw *withdraw, - MHD_RESULT *result) -{ - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int try = 0; try < 3; try++) - { - qs = TEH_plugin->get_withdraw (TEH_plugin->cls, - planchets_h, - withdraw); - 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_WITHDRAW_COMMITMENT_UNKNOWN, - NULL); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_HARD_ERROR: - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_withdraw"); - 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 unsuccessful retries*/ - GNUNET_break (0); - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_withdraw"); - return GNUNET_SYSERR; -} - - -/** - * @brief Derives an age-restricted planchet from a given secret and calculates the hash - * - * @param connection Connection to the client - * @param denom_key The denomination key - * @param secret The secret to a planchet - * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL - * @param nonce The derived nonce needed for CS denomination - * @param max_age The maximum age allowed - * @param[out] bch 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 enum GNUNET_GenericReturnValue -calculate_blinded_hash ( - struct MHD_Connection *connection, - struct TEH_DenominationKey *denom_key, - const struct TALER_PlanchetMasterSecretP *secret, - const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub, - union GNUNET_CRYPTO_BlindSessionNonce *nonce, - uint8_t max_age, - struct TALER_BlindedCoinHashP *bch, - MHD_RESULT *result) -{ - enum GNUNET_GenericReturnValue ret; - struct TALER_AgeCommitmentHashP ach; - - /* calculate age commitment hash */ - { - struct TALER_AgeCommitmentProof acp; - - TALER_age_restriction_from_secret (secret, - &denom_key->denom_pub.age_mask, - max_age, - &acp); - TALER_age_commitment_hash (&acp.commitment, - &ach); - TALER_age_commitment_proof_free (&acp); - } - - /* Next: calculate planchet */ - { - struct TALER_CoinPubHashP c_hash; - struct TALER_PlanchetDetail detail = {0}; - struct TALER_CoinSpendPrivateKeyP coin_priv; - union GNUNET_CRYPTO_BlindingSecretP bks; - struct GNUNET_CRYPTO_BlindingInputValues bi = { - .cipher = denom_key->denom_pub.bsign_pub_key->cipher - }; - struct TALER_ExchangeBlindingValues alg_values = { - .blinding_inputs = &bi - }; - - if (GNUNET_CRYPTO_BSA_CS == bi.cipher) - { - GNUNET_assert (NULL != r_pub); - GNUNET_assert (NULL != nonce); - bi.details.cs_values = *r_pub; - } - TALER_planchet_blinding_secret_create (secret, - &alg_values, - &bks); - TALER_planchet_setup_coin_priv (secret, - &alg_values, - &coin_priv); - ret = TALER_planchet_prepare (&denom_key->denom_pub, - &alg_values, - &bks, - nonce, - &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, - GNUNET_JSON_pack_string ( - "details", - "failed to prepare planchet from base key")); - return ret; - } - - TALER_coin_ev_hash (&detail.blinded_planchet, - &denom_key->h_denom_pub, - bch); - TALER_blinded_planchet_free (&detail.blinded_planchet); - } - - return ret; -} - - -/** - * @brief Checks the validity of the disclosed coins as follows: - * - Derives and calculates the disclosed coins' - * - public keys, - * - nonces (if applicable), - * - age commitments, - * - blindings - * - blinded hashes - * - Computes planchets_h with those calculated and the undisclosed hashes - * - Compares planchets_h with the value from the original commitment - * - Verifies that all public keys in indices larger than the age group - * corresponding to max_age are derived from the constant public key. - * - * The derivation of the blindings, (potential) nonces and age-commitment from - * a coin's private keys is defined in - * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw - * - * @param con HTTP-connection to the client - * @param wd Original withdraw request - * @param disclosed_batch_seeds The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many - * @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 *con, - const struct TALER_EXCHANGEDB_Withdraw *wd, - const struct TALER_RevealWithdrawMasterSeedsP *disclosed_batch_seeds, - MHD_RESULT *result) -{ - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - struct GNUNET_HashContext *hash_context; - struct TEH_KeyStateHandle *keys; - struct TEH_DenominationKey *denom_keys[wd->num_coins]; - struct TALER_Amount total_amount; - struct TALER_Amount total_fee; - struct TALER_AgeMask mask; - struct TALER_PlanchetMasterSecretP secrets[ - TALER_CNC_KAPPA - 1][wd->num_coins]; - bool is_cs[wd->num_coins]; - size_t cs_count = 0; - uint8_t secrets_idx = 0; /* first index into secrets */ - - GNUNET_assert (wd->noreveal_index < TALER_CNC_KAPPA); - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &total_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &total_fee)); - - memset (denom_keys, - 0, - sizeof(denom_keys)); - memset (is_cs, - 0, - sizeof(is_cs)); - - /* 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 (con, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - - /* Find the denomination keys */ - for (size_t i = 0; i < wd->num_coins; i++) - { - denom_keys[i] = - TEH_keys_denomination_by_serial_from_state ( - keys, - wd->denom_serials[i]); - if (NULL == denom_keys[i]) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - - /* Accumulate amount and fees */ - GNUNET_assert (0 <= TALER_amount_add (&total_amount, - &total_amount, - &denom_keys[i]->meta.value)); - GNUNET_assert (0 <= TALER_amount_add (&total_fee, - &total_fee, - &denom_keys[i]->meta.fees.withdraw)); - - if (i == 0) - mask = denom_keys[i]->meta.age_mask; - GNUNET_assert (mask.bits == denom_keys[i]->meta.age_mask.bits); - - if (GNUNET_CRYPTO_BSA_CS == - denom_keys[i]->denom_pub.bsign_pub_key->cipher) - { - is_cs[i] = true; - cs_count++; - } - } - - hash_context = GNUNET_CRYPTO_hash_context_start (); - - { - uint32_t cs_indices[cs_count]; - union GNUNET_CRYPTO_BlindSessionNonce nonces[cs_count]; - size_t cs_idx = 0; /* [0...cs_count) */ - - for (size_t i = 0; i < wd->num_coins; i++) - if (is_cs[i]) - cs_indices[cs_idx++] = i; - - TALER_cs_derive_only_cs_blind_nonces_from_seed (&wd->blinding_seed, - false, /* not for melt */ - cs_count, - cs_indices, - nonces); - - for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++) - { - if (gamma == wd->noreveal_index) - { - /** - * For the disclosed index, all we have to do is to accumulate the hash - * of the selected coins - */ - GNUNET_CRYPTO_hash_context_read ( - hash_context, - &wd->selected_h, - sizeof(wd->selected_h)); - } - else - { - /** - * For the non-disclosed index, we have to generate the planchets in - * this batch and calculate their hash - */ - struct GNUNET_HashContext *batch_ctx; - struct TALER_BlindedCoinHashP batch_h; - cs_idx = 0; - - batch_ctx = GNUNET_CRYPTO_hash_context_start (); - GNUNET_assert (NULL != batch_ctx); - - /* Expand the secrets for a disclosed batch */ - GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1)); - TALER_withdraw_expand_secrets ( - wd->num_coins, - &disclosed_batch_seeds->tuple[secrets_idx], - secrets[secrets_idx]); - - /** - * Now individually create each coin in this batch and calculate - * its hash, and accumulate the hash of the batch with it - */ - for (size_t coin_idx = 0; coin_idx < wd->num_coins; coin_idx++) - { - struct TALER_BlindedCoinHashP bch; - struct GNUNET_CRYPTO_CSPublicRPairP *rp; - union GNUNET_CRYPTO_BlindSessionNonce *np; - - if (is_cs[coin_idx]) - { - GNUNET_assert (cs_idx < cs_count); - np = &nonces[cs_idx]; - rp = &wd->cs_r_values[cs_idx]; - cs_idx++; - } - else - { - np = NULL; - rp = NULL; - } - ret = calculate_blinded_hash (con, - denom_keys[coin_idx], - &secrets[secrets_idx][coin_idx], - rp, - np, - wd->max_age, - &bch, - result); - if (GNUNET_OK != ret) - { - GNUNET_CRYPTO_hash_context_abort (hash_context); - return GNUNET_SYSERR; - } - /** - * Continue the running hash of all coin hashes in the batch - * with the calculated hash-value of the current, disclosed coin - */ - GNUNET_CRYPTO_hash_context_read (batch_ctx, - &bch, - sizeof(bch)); - } - /** - * Finalize the hash of this batch and add it to - * the total hash - */ - GNUNET_CRYPTO_hash_context_finish ( - batch_ctx, - &batch_h.hash); - GNUNET_CRYPTO_hash_context_read ( - hash_context, - &batch_h, - sizeof(batch_h)); - - secrets_idx++; - } - } - } - - /* Finally, compare the calculated hash with the original wd */ - { - struct TALER_HashBlindedPlanchetsP planchets_h; - - GNUNET_CRYPTO_hash_context_finish ( - hash_context, - &planchets_h.hash); - - if (0 != GNUNET_CRYPTO_hash_cmp ( - &wd->planchets_h.hash, - &planchets_h.hash)) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (con, - TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH, - NULL); - return GNUNET_SYSERR; - } - - } - return GNUNET_OK; -} - - -/** - * @brief Send a response for "/reveal-withdraw" - * - * @param connection The http connection to the client to send the response to - * @param commitment The data from the commitment with signatures - * @return a MHD result code - */ -static MHD_RESULT -reply_withdraw_reveal_success ( - struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_Withdraw *commitment) -{ - json_t *list = json_array (); - - GNUNET_assert (NULL != list); - for (size_t i = 0; i < commitment->num_coins; i++) - { - json_t *obj = GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_denom_sig (NULL, - &commitment->denom_sigs[i])); - GNUNET_assert (0 == - json_array_append_new (list, - obj)); - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("ev_sigs", - list)); -} - - -MHD_RESULT -TEH_handler_reveal_withdraw ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]) -{ - MHD_RESULT result = MHD_NO; - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - struct WithdrawRevealContext actx = {0}; - const json_t *j_disclosed_batch_seeds; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("planchets_h", - &actx.planchets_h), - GNUNET_JSON_spec_array_const ("disclosed_batch_seeds", - &j_disclosed_batch_seeds), - GNUNET_JSON_spec_end () - }; - - (void) args; - /* Parse JSON body*/ - ret = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; - } - - do { - /* Extract denominations, blinded and disclosed coins */ - if (GNUNET_OK != - parse_withdraw_reveal_json ( - rc->connection, - j_disclosed_batch_seeds, - &actx, - &result)) - break; - - /* Find original commitment */ - if (GNUNET_OK != - find_original_withdraw ( - rc->connection, - &actx.planchets_h, - &actx.withdraw, - &result)) - break; - - /* Verify the computed planchets_h 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.withdraw, - &actx.disclosed_batch_seeds, - &result)) - break; - - /* Finally, return the signatures */ - result = reply_withdraw_reveal_success (rc->connection, - &actx.withdraw); - - } while (0); - - GNUNET_JSON_parse_free (spec); - if (NULL != actx.withdraw.denom_sigs) - { - for (size_t i = 0; i<actx.withdraw.num_coins; i++) - TALER_blinded_denom_sig_free (&actx.withdraw.denom_sigs[i]); - GNUNET_free (actx.withdraw.denom_sigs); - } - GNUNET_free (actx.withdraw.cs_r_values); - GNUNET_free (actx.withdraw.denom_pub_hashes); - GNUNET_free (actx.withdraw.denom_serials); - return result; -} - - -/* end of taler-exchange-httpd_reveal_withdraw.c */ diff --git a/src/exchange/taler-exchange-httpd_reveal-withdraw.h b/src/exchange/taler-exchange-httpd_reveal-withdraw.h @@ -1,56 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_reveal-withdraw.h - * @brief Handle /reveal-withdraw/$ACH requests - * @author Özgür Kesim - */ -#ifndef TALER_EXCHANGE_HTTPD_REVEAL_WITHDRAW_H -#define TALER_EXCHANGE_HTTPD_REVEAL_WITHDRAW_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/reveal-withdraw" request. - * - * The client got a noreveal_index in response to a previous request - * /withdraw. It now has to reveal all n*(kappa-1) - * coin's private keys (except for the noreveal_index), from which all other - * coin-relevant data (blinding, age restriction, nonce) is derived from. - * - * The exchange computes those values, ensures that the maximum age is - * correctly applied, calculates the hash of the blinded envelopes, and - - * together with the non-disclosed blinded envelopes - compares the hash of - * the calculated withdraw commitment against the original. - * - * If all those checks and the used denominations turn out to be correct, the - * exchange signs all blinded envelopes with their appropriate denomination - * keys. - * - * @param rc request context - * @param root uploaded JSON data - * @param args not used - * @return MHD result code - */ -MHD_RESULT -TEH_handler_reveal_withdraw ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]); - -#endif diff --git a/src/exchange/taler-exchange-httpd_spa.c b/src/exchange/taler-exchange-httpd_spa.c @@ -1,128 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2020, 2023, 2024 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of EXCHANGEABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_spa.c - * @brief logic to load single page apps (/) - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include "taler/taler_util.h" -#include "taler/taler_mhd_lib.h" -#include <gnunet/gnunet_mhd_compat.h> -#include "taler-exchange-httpd.h" -#include "taler-exchange-httpd_spa.h" - - -/** - * Resources of the AML SPA. - */ -static struct TALER_MHD_Spa *aml_spa; - -/** - * Resources of the KYC SPA. - */ -static struct TALER_MHD_Spa *kyc_spa; - - -MHD_RESULT -TEH_handler_aml_spa (struct TEH_RequestContext *rc, - const char *const args[]) -{ - const char *path = args[0]; - - return TALER_MHD_spa_handler (aml_spa, - rc->connection, - path); -} - - -MHD_RESULT -TEH_handler_kyc_spa (struct TEH_RequestContext *rc, - const char *const args[]) -{ - const char *path = args[0]; - struct TALER_AccountAccessTokenP tok; - - if (NULL == path) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT, - "no account access token specified"); - } - if (GNUNET_OK == - GNUNET_STRINGS_string_to_data (path, - strlen (path), - &tok, - sizeof (tok))) - { - /* The access token is used internally by the SPA, - we simply map all access tokens to "index.html" */ - path = "index.html"; - } - return TALER_MHD_spa_handler (kyc_spa, - rc->connection, - path); -} - - -enum GNUNET_GenericReturnValue -TEH_spa_init () -{ - aml_spa = TALER_MHD_spa_load (TALER_EXCHANGE_project_data (), - "aml-spa/"); - if (NULL == aml_spa) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - kyc_spa = TALER_MHD_spa_load (TALER_EXCHANGE_project_data (), - "kyc-spa/"); - if (NULL == kyc_spa) - { - GNUNET_break (0); - TALER_MHD_spa_free (aml_spa); - aml_spa = NULL; - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/* Suppresses warning */ -void __attribute__ ((destructor)) -get_spa_fini (void); - -/** - * Nicely shut down. - */ -void __attribute__ ((destructor)) -get_spa_fini (void) -{ - if (NULL != kyc_spa) - { - TALER_MHD_spa_free (kyc_spa); - kyc_spa = NULL; - } - if (NULL != aml_spa) - { - TALER_MHD_spa_free (aml_spa); - aml_spa = NULL; - } -} diff --git a/src/exchange/taler-exchange-httpd_spa.h b/src/exchange/taler-exchange-httpd_spa.h @@ -1,63 +0,0 @@ -/* - 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 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 EXCHANGEABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_spa.h - * @brief logic to preload and serve static files - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_SPA_H -#define TALER_EXCHANGE_HTTPD_SPA_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Return our single-page-app user interface - * for AML staff (see contrib/wallet-core/). - * - * @param rc context of the handler - * @param[in,out] args remaining arguments (ignored) - * @return #MHD_YES on success (reply queued), #MHD_NO on error (close connection) - */ -MHD_RESULT -TEH_handler_aml_spa (struct TEH_RequestContext *rc, - const char *const args[]); - - -/** - * Return our single-page-app user interface - * for the KYC process (see contrib/wallet-core/). - * - * @param rc context of the handler - * @param[in,out] args remaining arguments (ignored) - * @return #MHD_YES on success (reply queued), #MHD_NO on error (close connection) - */ -MHD_RESULT -TEH_handler_kyc_spa (struct TEH_RequestContext *rc, - const char *const args[]); - - -/** - * Preload and compress SPA files. - * - * @return #GNUNET_OK on success - */ -enum GNUNET_GenericReturnValue -TEH_spa_init (void); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_terms.c b/src/exchange/taler-exchange-httpd_terms.c @@ -1,82 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019, 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_terms.c - * @brief Handle /terms requests to return the terms of service - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_terms.h" - -/** - * Our terms of service. - */ -static struct TALER_MHD_Legal *tos; - - -/** - * Our privacy policy. - */ -static struct TALER_MHD_Legal *pp; - - -MHD_RESULT -TEH_handler_terms (struct TEH_RequestContext *rc, - const char *const args[]) -{ - (void) args; - return TALER_MHD_reply_legal (rc->connection, - tos); -} - - -MHD_RESULT -TEH_handler_privacy (struct TEH_RequestContext *rc, - const char *const args[]) -{ - (void) args; - return TALER_MHD_reply_legal (rc->connection, - pp); -} - - -void -TEH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg) -{ - tos = TALER_MHD_legal_load (cfg, - "exchange", - "TERMS_DIR", - "TERMS_ETAG"); - if (NULL == tos) - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Terms of service not configured\n"); - pp = TALER_MHD_legal_load (cfg, - "exchange", - "PRIVACY_DIR", - "PRIVACY_ETAG"); - if (NULL == pp) - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Privacy policy not configured\n"); -} - - -/* end of taler-exchange-httpd_terms.c */ diff --git a/src/exchange/taler-exchange-httpd_terms.h b/src/exchange/taler-exchange-httpd_terms.h @@ -1,65 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019, 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_terms.h - * @brief Handle /terms requests to return the terms of service - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_TERMS_H -#define TALER_EXCHANGE_HTTPD_TERMS_H -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_responses.h" - - -/** - * Handle a "/terms" request. - * - * @param rc request context - * @param args array of additional options (must be empty for this function) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_terms (struct TEH_RequestContext *rc, - const char *const args[]); - - -/** - * Handle a "/privacy" request. - * - * @param rc request context - * @param args array of additional options (must be empty for this function) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_privacy (struct TEH_RequestContext *rc, - const char *const args[]); - - -/** - * Load our terms of service as per configuration. - * - * @param cfg configuration to process - */ -void -TEH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_transfers_get.c b/src/exchange/taler-exchange-httpd_transfers_get.c @@ -1,641 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2018, 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_transfers_get.c - * @brief Handle wire transfer(s) GET requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <pthread.h> -#include "taler/taler_signatures.h" -#include "taler-exchange-httpd_keys.h" -#include "taler-exchange-httpd_transfers_get.h" -#include "taler-exchange-httpd_responses.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" - - -/** - * Information about one of the transactions that was - * aggregated, to be returned in the /transfers response. - */ -struct AggregatedDepositDetail -{ - - /** - * We keep deposit details in a DLL. - */ - struct AggregatedDepositDetail *next; - - /** - * We keep deposit details in a DLL. - */ - struct AggregatedDepositDetail *prev; - - /** - * Hash of the contract terms. - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Coin's public key of the deposited coin. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Total value of the coin in the deposit (after - * refunds). - */ - struct TALER_Amount deposit_value; - - /** - * Fees charged by the exchange for the deposit of this coin (possibly after reduction due to refunds). - */ - struct TALER_Amount deposit_fee; - - /** - * Total amount refunded for this coin. - */ - struct TALER_Amount refund_total; -}; - - -/** - * A merchant asked for transaction details about a wire transfer. - * Provide them. Generates the 200 reply. - * - * @param connection connection to the client - * @param total total amount that was transferred - * @param merchant_pub public key of the merchant - * @param payto_uri destination account - * @param wire_fee wire fee that was charged - * @param exec_time execution time of the wire transfer - * @param wdd_head linked list with details about the combined deposits - * @return MHD result code - */ -static MHD_RESULT -reply_transfer_details (struct MHD_Connection *connection, - const struct TALER_Amount *total, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const struct TALER_FullPayto payto_uri, - const struct TALER_Amount *wire_fee, - struct GNUNET_TIME_Timestamp exec_time, - const struct AggregatedDepositDetail *wdd_head) -{ - json_t *deposits; - struct GNUNET_HashContext *hash_context; - struct GNUNET_HashCode h_details; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - struct TALER_FullPaytoHashP h_payto; - - deposits = json_array (); - GNUNET_assert (NULL != deposits); - hash_context = GNUNET_CRYPTO_hash_context_start (); - for (const struct AggregatedDepositDetail *wdd_pos = wdd_head; - NULL != wdd_pos; - wdd_pos = wdd_pos->next) - { - TALER_exchange_online_wire_deposit_append (hash_context, - &wdd_pos->h_contract_terms, - exec_time, - &wdd_pos->coin_pub, - &wdd_pos->deposit_value, - &wdd_pos->deposit_fee); - if (0 != - json_array_append_new ( - deposits, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("h_contract_terms", - &wdd_pos->h_contract_terms), - GNUNET_JSON_pack_data_auto ("coin_pub", - &wdd_pos->coin_pub), - - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("refund_total", - TALER_amount_is_zero ( - &wdd_pos->refund_total) - ? NULL - : &wdd_pos->refund_total)), - TALER_JSON_pack_amount ("deposit_value", - &wdd_pos->deposit_value), - TALER_JSON_pack_amount ("deposit_fee", - &wdd_pos->deposit_fee)))) - { - json_decref (deposits); - GNUNET_CRYPTO_hash_context_abort (hash_context); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - "json_array_append_new() failed"); - } - } - GNUNET_CRYPTO_hash_context_finish (hash_context, - &h_details); - { - enum TALER_ErrorCode ec; - - if (TALER_EC_NONE != - (ec = TALER_exchange_online_wire_deposit_sign ( - &TEH_keys_exchange_sign_, - total, - wire_fee, - merchant_pub, - payto_uri, - &h_details, - &pub, - &sig))) - { - json_decref (deposits); - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - } - - TALER_full_payto_hash (payto_uri, - &h_payto); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("total", - total), - TALER_JSON_pack_amount ("wire_fee", - wire_fee), - GNUNET_JSON_pack_data_auto ("merchant_pub", - merchant_pub), - GNUNET_JSON_pack_data_auto ("h_payto", - &h_payto), - GNUNET_JSON_pack_timestamp ("execution_time", - exec_time), - GNUNET_JSON_pack_array_steal ("deposits", - deposits), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); -} - - -/** - * Closure for #handle_transaction_data. - */ -struct WtidTransactionContext -{ - - /** - * Identifier of the wire transfer to track. - */ - struct TALER_WireTransferIdentifierRawP wtid; - - /** - * Total amount of the wire transfer, as calculated by - * summing up the individual amounts. To be rounded down - * to calculate the real transfer amount at the end. - * Only valid if @e is_valid is #GNUNET_YES. - */ - struct TALER_Amount total; - - /** - * Public key of the merchant, only valid if @e is_valid - * is #GNUNET_YES. - */ - struct TALER_MerchantPublicKeyP merchant_pub; - - /** - * Wire fees applicable at @e exec_time. - */ - struct TALER_WireFeeSet fees; - - /** - * Execution time of the wire transfer - */ - struct GNUNET_TIME_Timestamp exec_time; - - /** - * Head of DLL with deposit details for transfers GET response. - */ - struct AggregatedDepositDetail *wdd_head; - - /** - * Tail of DLL with deposit details for transfers GET response. - */ - struct AggregatedDepositDetail *wdd_tail; - - /** - * Where were the funds wired? - */ - struct TALER_FullPayto payto_uri; - - /** - * JSON array with details about the individual deposits. - */ - json_t *deposits; - - /** - * Initially #GNUNET_NO, if we found no deposits so far. Set to - * #GNUNET_YES if we got transaction data, and the database replies - * remained consistent with respect to @e merchant_pub and @e h_wire - * (as they should). Set to #GNUNET_SYSERR if we encountered an - * internal error. - */ - enum GNUNET_GenericReturnValue is_valid; - -}; - - -/** - * Callback that totals up the applicable refunds. - * - * @param cls a `struct TALER_Amount` where we keep the total - * @param amount_with_fee amount being refunded - */ -static enum GNUNET_GenericReturnValue -add_refunds (void *cls, - const struct TALER_Amount *amount_with_fee) - -{ - struct TALER_Amount *total = cls; - - if (0 > - TALER_amount_add (total, - total, - amount_with_fee)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Function called with the results of the lookup of the individual deposits - * that were aggregated for the given wire transfer. - * - * @param cls our context for transmission - * @param rowid which row in the DB is the information from (for diagnostics), ignored - * @param merchant_pub public key of the merchant (should be same for all callbacks with the same @e cls) - * @param account_payto_uri where the funds were sent - * @param h_payto hash over @a account_payto_uri as it is in the DB - * @param exec_time execution time of the wire transfer (should be same for all callbacks with the same @e cls) - * @param h_contract_terms which proposal was this payment about - * @param denom_pub denomination public key of the @a coin_pub (ignored) - * @param coin_pub which public key was this payment about - * @param deposit_value amount contributed by this coin in total (including fee) - * @param deposit_fee deposit fee charged by exchange for this coin - */ -static void -handle_deposit_data ( - void *cls, - uint64_t rowid, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const struct TALER_FullPayto account_payto_uri, - const struct TALER_FullPaytoHashP *h_payto, - struct GNUNET_TIME_Timestamp exec_time, - const struct TALER_PrivateContractHashP *h_contract_terms, - const struct TALER_DenominationPublicKey *denom_pub, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *deposit_value, - const struct TALER_Amount *deposit_fee) -{ - struct WtidTransactionContext *ctx = cls; - struct TALER_Amount total_refunds; - struct TALER_Amount dval; - struct TALER_Amount dfee; - enum GNUNET_DB_QueryStatus qs; - - (void) rowid; - (void) denom_pub; - (void) h_payto; - if (GNUNET_SYSERR == ctx->is_valid) - return; - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (deposit_value->currency, - &total_refunds)); - qs = TEH_plugin->select_refunds_by_coin (TEH_plugin->cls, - coin_pub, - merchant_pub, - h_contract_terms, - &add_refunds, - &total_refunds); - if (qs < 0) - { - GNUNET_break (0); - ctx->is_valid = GNUNET_SYSERR; - return; - } - if (1 == - TALER_amount_cmp (&total_refunds, - deposit_value)) - { - /* Refunds exceeded total deposit? not OK! */ - GNUNET_break (0); - ctx->is_valid = GNUNET_SYSERR; - return; - } - if (0 == - TALER_amount_cmp (&total_refunds, - deposit_value)) - { - /* total_refunds == deposit_value; - in this case, the total contributed to the - wire transfer is zero (as are fees) */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (deposit_value->currency, - &dval)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (deposit_value->currency, - &dfee)); - - } - else - { - /* Compute deposit value by subtracting refunds */ - GNUNET_assert (0 < - TALER_amount_subtract (&dval, - deposit_value, - &total_refunds)); - if (-1 == - TALER_amount_cmp (&dval, - deposit_fee)) - { - /* dval < deposit_fee, so after refunds less than - the deposit fee remains; reduce deposit fee to - the remaining value of the coin */ - dfee = dval; - } - else - { - /* Partial refund, deposit fee remains */ - dfee = *deposit_fee; - } - } - - if (GNUNET_NO == ctx->is_valid) - { - /* First one we encounter, setup general information in 'ctx' */ - ctx->merchant_pub = *merchant_pub; - ctx->payto_uri.full_payto = GNUNET_strdup (account_payto_uri.full_payto); - ctx->exec_time = exec_time; - ctx->is_valid = GNUNET_YES; - if (0 > - TALER_amount_subtract (&ctx->total, - &dval, - &dfee)) - { - GNUNET_break (0); - ctx->is_valid = GNUNET_SYSERR; - return; - } - } - else - { - struct TALER_Amount delta; - - /* Subsequent data, check general information matches that in 'ctx'; - (it should, otherwise the deposits should not have been aggregated) */ - if ( (0 != GNUNET_memcmp (&ctx->merchant_pub, - merchant_pub)) || - (0 != TALER_full_payto_cmp (account_payto_uri, - ctx->payto_uri)) ) - { - GNUNET_break (0); - ctx->is_valid = GNUNET_SYSERR; - return; - } - if (0 > - TALER_amount_subtract (&delta, - &dval, - &dfee)) - { - GNUNET_break (0); - ctx->is_valid = GNUNET_SYSERR; - return; - } - if (0 > - TALER_amount_add (&ctx->total, - &ctx->total, - &delta)) - { - GNUNET_break (0); - ctx->is_valid = GNUNET_SYSERR; - return; - } - } - - { - struct AggregatedDepositDetail *wdd; - - wdd = GNUNET_new (struct AggregatedDepositDetail); - wdd->deposit_value = dval; - wdd->deposit_fee = dfee; - wdd->refund_total = total_refunds; - wdd->h_contract_terms = *h_contract_terms; - wdd->coin_pub = *coin_pub; - GNUNET_CONTAINER_DLL_insert (ctx->wdd_head, - ctx->wdd_tail, - wdd); - } -} - - -/** - * Free data structure reachable from @a ctx, but not @a ctx itself. - * - * @param ctx context to free - */ -static void -free_ctx (struct WtidTransactionContext *ctx) -{ - struct AggregatedDepositDetail *wdd; - - while (NULL != (wdd = ctx->wdd_head)) - { - GNUNET_CONTAINER_DLL_remove (ctx->wdd_head, - ctx->wdd_tail, - wdd); - GNUNET_free (wdd); - } - GNUNET_free (ctx->payto_uri.full_payto); -} - - -/** - * Execute a "/transfers" GET operation. Returns the deposit details of the - * deposits that were aggregated to create the given wire transfer. - * - * 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 closure - * @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 -get_transfer_deposits (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct WtidTransactionContext *ctx = cls; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp wire_fee_start_date; - struct GNUNET_TIME_Timestamp wire_fee_end_date; - struct TALER_MasterSignatureP wire_fee_master_sig; - - /* resetting to NULL/0 in case transaction was repeated after - serialization failure */ - free_ctx (ctx); - qs = TEH_plugin->lookup_wire_transfer (TEH_plugin->cls, - &ctx->wtid, - &handle_deposit_data, - ctx); - if (0 > qs) - { - 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, - "wire transfer"); - } - return qs; - } - if (GNUNET_SYSERR == ctx->is_valid) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "wire history malformed"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (GNUNET_NO == ctx->is_valid) - { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - { - char *wire_method; - uint64_t rowid; - - wire_method = TALER_payto_get_method (ctx->payto_uri.full_payto); - if (NULL == wire_method) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "payto:// without wire method encountered"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - qs = TEH_plugin->get_wire_fee (TEH_plugin->cls, - wire_method, - ctx->exec_time, - &rowid, - &wire_fee_start_date, - &wire_fee_end_date, - &ctx->fees, - &wire_fee_master_sig); - GNUNET_free (wire_method); - } - if (0 >= qs) - { - if ( (GNUNET_DB_STATUS_HARD_ERROR == qs) || - (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND, - NULL); - } - return qs; - } - if (0 > - TALER_amount_subtract (&ctx->total, - &ctx->total, - &ctx->fees.wire)) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; -} - - -MHD_RESULT -TEH_handler_transfers_get (struct TEH_RequestContext *rc, - const char *const args[1]) -{ - struct WtidTransactionContext ctx; - MHD_RESULT mhd_ret; - - memset (&ctx, - 0, - sizeof (ctx)); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &ctx.wtid, - sizeof (ctx.wtid))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_TRANSFERS_GET_WTID_MALFORMED, - args[0]); - } - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "run transfers GET", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &get_transfer_deposits, - &ctx)) - { - free_ctx (&ctx); - return mhd_ret; - } - mhd_ret = reply_transfer_details (rc->connection, - &ctx.total, - &ctx.merchant_pub, - ctx.payto_uri, - &ctx.fees.wire, - ctx.exec_time, - ctx.wdd_head); - free_ctx (&ctx); - return mhd_ret; -} - - -/* end of taler-exchange-httpd_transfers_get.c */ diff --git a/src/exchange/taler-exchange-httpd_transfers_get.h b/src/exchange/taler-exchange-httpd_transfers_get.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2017, 2021 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_transfers_get.h - * @brief Handle wire transfer tracking-related requests - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_TRANSFERS_GET_H -#define TALER_EXCHANGE_HTTPD_TRANSFERS_GET_H - -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a GET "/transfers/$WTID" request. - * - * @param rc request context of the handler - * @param args array of additional options (length: 1, just the wtid) - * @return MHD result code - */ -MHD_RESULT -TEH_handler_transfers_get (struct TEH_RequestContext *rc, - const char *const args[1]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c @@ -1,1818 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_withdraw.c - * @brief Code to handle /withdraw requests - * @note This endpoint is active since v26 of the protocol API - * @author Özgür Kesim - */ - -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler-exchange-httpd.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_kyclogic_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_withdraw.h" -#include "taler-exchange-httpd_common_kyc.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" -#include "taler/taler_util.h" - -/** - * The different type of errors that might occur, sorted by name. - * Some of them require idempotency checks, which are marked - * in @e idempotency_check_required below. - */ -enum WithdrawError -{ - WITHDRAW_ERROR_NONE, - WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED, - WITHDRAW_ERROR_AMOUNT_OVERFLOW, - WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, - WITHDRAW_ERROR_BLINDING_SEED_REQUIRED, - WITHDRAW_ERROR_CIPHER_MISMATCH, - WITHDRAW_ERROR_CONFIRMATION_SIGN, - WITHDRAW_ERROR_DB_FETCH_FAILED, - WITHDRAW_ERROR_DB_INVARIANT_FAILURE, - WITHDRAW_ERROR_DENOMINATION_EXPIRED, - WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, - WITHDRAW_ERROR_DENOMINATION_REVOKED, - WITHDRAW_ERROR_DENOMINATION_SIGN, - WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - WITHDRAW_ERROR_FEE_OVERFLOW, - WITHDRAW_ERROR_IDEMPOTENT_PLANCHET, - WITHDRAW_ERROR_INSUFFICIENT_FUNDS, - WITHDRAW_ERROR_CRYPTO_HELPER, - WITHDRAW_ERROR_KEYS_MISSING, - WITHDRAW_ERROR_KYC_REQUIRED, - WITHDRAW_ERROR_LEGITIMIZATION_RESULT, - WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE, - WITHDRAW_ERROR_NONCE_REUSE, - WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, - WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN, - WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID, - WITHDRAW_ERROR_RESERVE_UNKNOWN, -}; - -/** - * With the bits set in this value will be mark the errors - * that require a check for idempotency before actually - * returning an error. - */ -static const uint64_t idempotency_check_required = - 0 - | (1LLU << WITHDRAW_ERROR_DENOMINATION_EXPIRED) - | (1LLU << WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN) - | (1LLU << WITHDRAW_ERROR_DENOMINATION_REVOKED) - | (1LLU << WITHDRAW_ERROR_INSUFFICIENT_FUNDS) - | (1LLU << WITHDRAW_ERROR_KEYS_MISSING) - | (1LLU << WITHDRAW_ERROR_KYC_REQUIRED); - -#define IDEMPOTENCY_CHECK_REQUIRED(ec) \ - (0LLU != (idempotency_check_required & (1LLU << (ec)))) - - -/** - * Context for a /withdraw requests - */ -struct WithdrawContext -{ - - /** - * This struct is kept in a DLL. - */ - struct WithdrawContext *prev; - struct WithdrawContext *next; - - /** - * Processing phase we are in. - * The ordering here partially matters, as we progress through - * them by incrementing the phase in the happy path. - */ - enum - { - WITHDRAW_PHASE_PARSE = 0, - WITHDRAW_PHASE_CHECK_KEYS, - WITHDRAW_PHASE_CHECK_RESERVE_SIGNATURE, - WITHDRAW_PHASE_RUN_LEGI_CHECK, - WITHDRAW_PHASE_SUSPENDED, - WITHDRAW_PHASE_CHECK_KYC_RESULT, - WITHDRAW_PHASE_PREPARE_TRANSACTION, - WITHDRAW_PHASE_RUN_TRANSACTION, - WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS, - WITHDRAW_PHASE_GENERATE_REPLY_ERROR, - WITHDRAW_PHASE_RETURN_NO, - WITHDRAW_PHASE_RETURN_YES, - } phase; - - - /** - * Handle for the legitimization check. - */ - struct TEH_LegitimizationCheckHandle *lch; - - /** - * Request context - */ - const struct TEH_RequestContext *rc; - - /** - * KYC status for the operation. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; - - /** - * Current time for the DB transaction. - */ - struct GNUNET_TIME_Timestamp now; - - /** - * Set to the hash of the normalized payto URI that established - * the reserve. - */ - struct TALER_NormalizedPaytoHashP h_normalized_payto; - - /** - * Captures all parameters provided in the JSON request - */ - struct - { - /** - * All fields (from the request or computed) - * that we persist in the database. - */ - struct TALER_EXCHANGEDB_Withdraw withdraw; - - /** - * In some error cases we check for idempotency. - * If we find an entry in the database, we mark this here. - */ - bool is_idempotent; - - /** - * In some error conditions the request is checked - * for idempotency and the result from the database - * is stored here. - */ - struct TALER_EXCHANGEDB_Withdraw withdraw_idem; - - /** - * Array of ``withdraw.num_coins`` hashes of the public keys - * of the denominations to withdraw. - */ - struct TALER_DenominationHashP *denoms_h; - - /** - * Number of planchets. If ``withdraw.max_age`` was _not_ set, this is equal to ``num_coins``. - * Otherwise (``withdraw.max_age`` was set) it is ``withdraw.num_coins * kappa``. - */ - size_t num_planchets; - - /** - * Array of ``withdraw.num_planchets`` coin planchets. - * Note that the size depends on the age restriction: - * If ``withdraw.age_proof_required`` is false, - * this is an array of length ``withdraw.num_coins``. - * Otherwise it is an array of length ``kappa*withdraw.num_coins``, - * arranged in runs of ``num_coins`` coins, - * [0..num_coins)..[0..num_coins), - * one for each #TALER_CNC_KAPPA value. - */ - struct TALER_BlindedPlanchet *planchets; - - /** - * If proof of age-restriction is required, the #TALER_CNC_KAPPA hashes - * of the batches of ``withdraw.num_coins`` coins. - */ - struct TALER_HashBlindedPlanchetsP kappa_planchets_h[TALER_CNC_KAPPA]; - - /** - * Total (over all coins) amount (excluding fee) committed to withdraw - */ - struct TALER_Amount amount; - - /** - * Total fees for the withdraw - */ - struct TALER_Amount fee; - - /** - * Array of length ``withdraw.num_cs_r_values`` of indices into - * @e denoms_h of CS denominations. - */ - uint32_t *cs_indices; - - } request; - - - /** - * Errors occurring during evaluation of the request are captured in this - * struct. In phase WITHDRAW_PHASE_GENERATE_REPLY_ERROR an appropriate error - * message is prepared and sent to the client. - */ - struct - { - /* The (internal) error code */ - enum WithdrawError code; - - /** - * Some errors require details to be sent to the client. - * These are captured in this union. - * Each field is named according to the error that is using it, except - * commented otherwise. - */ - union - { - const char *request_parameter_malformed; - - const char *reserve_cipher_unknown; - - /** - * For all errors related to a particular denomination, i.e. - * WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, - * WITHDRAW_ERROR_DENOMINATION_EXPIRED, - * WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - * WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - * we use this one field. - */ - const struct TALER_DenominationHashP *denom_h; - - const char *db_fetch_context; - - struct - { - uint16_t max_allowed; - uint32_t birthday; - } maximum_age_too_large; - - /** - * The lowest age required - */ - uint16_t age_restriction_required; - - /** - * Balance of the reserve - */ - struct TALER_Amount insufficient_funds; - - enum TALER_ErrorCode ec_confirmation_sign; - - enum TALER_ErrorCode ec_denomination_sign; - - struct - { - struct MHD_Response *response; - unsigned int http_status; - } legitimization_result; - - } details; - } error; -}; - -/** - * The following macros set the given error code, - * set the phase to WITHDRAW_PHASE_GENERATE_REPLY_ERROR, - * and optionally set the given field (with an optionally given value). - */ -#define SET_ERROR(wc, ec) \ - do \ - { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \ - (wc)->error.code = (ec); \ - (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0) - -#define SET_ERROR_WITH_FIELD(wc, ec, field) \ - do \ - { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \ - (wc)->error.code = (ec); \ - (wc)->error.details.field = (field); \ - (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0) - -#define SET_ERROR_WITH_DETAIL(wc, ec, field, value) \ - do \ - { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \ - (wc)->error.code = (ec); \ - (wc)->error.details.field = (value); \ - (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0) - - -/** - * All withdraw context is kept in a DLL. - */ -static struct WithdrawContext *wc_head; -static struct WithdrawContext *wc_tail; - - -void -TEH_withdraw_cleanup () -{ - struct WithdrawContext *wc; - - while (NULL != (wc = wc_head)) - { - GNUNET_CONTAINER_DLL_remove (wc_head, - wc_tail, - wc); - wc->phase = WITHDRAW_PHASE_RETURN_NO; - MHD_resume_connection (wc->rc->connection); - } -} - - -/** - * Terminate the main loop by returning the final - * result. - * - * @param[in,out] wc context to update phase for - * @param mres MHD status to return - */ -static void -finish_loop (struct WithdrawContext *wc, - MHD_RESULT mres) -{ - wc->phase = (MHD_YES == mres) - ? WITHDRAW_PHASE_RETURN_YES - : WITHDRAW_PHASE_RETURN_NO; -} - - -/** - * Check if the withdraw request is replayed - * and we already have an answer. - * If so, replay the existing answer and return the HTTP response. - * - * @param[in,out] wc parsed request data - * @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 -withdraw_is_idempotent ( - struct WithdrawContext *wc) -{ - enum GNUNET_DB_QueryStatus qs; - uint8_t max_retries = 3; - - /* We should at most be called once */ - GNUNET_assert (! wc->request.is_idempotent); - while (0 < max_retries--) - { - qs = TEH_plugin->get_withdraw ( - TEH_plugin->cls, - &wc->request.withdraw.planchets_h, - &wc->request.withdraw_idem); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - - if (0 > qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_DB_FETCH_FAILED, - db_fetch_context, - "get_withdraw"); - return true; /* Well, kind-of. */ - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return false; - - wc->request.is_idempotent = true; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "request is idempotent\n"); - - /* Generate idempotent reply */ - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW]++; - wc->phase = WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS; - return true; -} - - -/** - * Function implementing 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 WithdrawContext *` - * @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 -withdraw_transaction ( - void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct WithdrawContext *wc = cls; - enum GNUNET_DB_QueryStatus qs; - bool balance_ok; - bool age_ok; - bool found; - uint16_t noreveal_index; - bool nonce_reuse; - uint16_t allowed_maximum_age; - uint32_t reserve_birthday; - struct TALER_Amount insufficient_funds; - - qs = TEH_plugin->do_withdraw (TEH_plugin->cls, - &wc->request.withdraw, - &wc->now, - &balance_ok, - &insufficient_funds, - &age_ok, - &allowed_maximum_age, - &reserve_birthday, - &found, - &noreveal_index, - &nonce_reuse); - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_DB_FETCH_FAILED, - db_fetch_context, - "do_withdraw"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - SET_ERROR (wc, - WITHDRAW_ERROR_RESERVE_UNKNOWN); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (found) - { - /** - * The request was idempotent and we got the previous noreveal_index. - * We simply overwrite that value in our current withdraw object and - * move on to reply success. - */ - wc->request.withdraw.noreveal_index = noreveal_index; - wc->phase = WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - } - - if (! age_ok) - { - if (wc->request.withdraw.age_proof_required) - { - wc->error.details.maximum_age_too_large.max_allowed = allowed_maximum_age; - wc->error.details.maximum_age_too_large.birthday = reserve_birthday; - SET_ERROR (wc, - WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE); - } - else - { - wc->error.details.age_restriction_required = allowed_maximum_age; - SET_ERROR (wc, - WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED); - } - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (! balance_ok) - { - TEH_plugin->rollback (TEH_plugin->cls); - SET_ERROR_WITH_FIELD (wc, - WITHDRAW_ERROR_INSUFFICIENT_FUNDS, - insufficient_funds); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (nonce_reuse) - { - GNUNET_break (0); - SET_ERROR (wc, - WITHDRAW_ERROR_NONCE_REUSE); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++; - return qs; -} - - -/** - * The request was prepared successfully. - * Run the main DB transaction. - * - * @param wc The context for the current withdraw request - */ -static void -phase_run_transaction ( - struct WithdrawContext *wc) -{ - MHD_RESULT mhd_ret; - enum GNUNET_GenericReturnValue qs; - - GNUNET_assert (WITHDRAW_PHASE_RUN_TRANSACTION == - wc->phase); - qs = TEH_DB_run_transaction (wc->rc->connection, - "run withdraw", - TEH_MT_REQUEST_WITHDRAW, - &mhd_ret, - &withdraw_transaction, - wc); - if (WITHDRAW_PHASE_RUN_TRANSACTION != wc->phase) - return; - GNUNET_break (GNUNET_OK == qs); - /* If the transaction has changed the phase, we don't alter it and return.*/ - wc->phase++; -} - - -/** - * The request for withdraw was parsed successfully. - * Sign and persist the chosen blinded coins for the reveal step. - * - * @param wc The context for the current withdraw request - */ -static void -phase_prepare_transaction ( - struct WithdrawContext *wc) -{ - size_t offset = 0; - - wc->request.withdraw.denom_sigs - = GNUNET_new_array ( - wc->request.withdraw.num_coins, - struct TALER_BlindedDenominationSignature); - /* Pick the challenge in case of age restriction */ - if (wc->request.withdraw.age_proof_required) - { - wc->request.withdraw.noreveal_index = - GNUNET_CRYPTO_random_u32 ( - GNUNET_CRYPTO_QUALITY_STRONG, - TALER_CNC_KAPPA); - /** - * In case of age restriction, we use the corresponding offset in the planchet - * array to the beginning of the coins corresponding to the noreveal_index. - */ - offset = wc->request.withdraw.noreveal_index - * wc->request.withdraw.num_coins; - GNUNET_assert (offset + wc->request.withdraw.num_coins <= - wc->request.num_planchets); - } - - /* Choose and sign the coins */ - { - struct TEH_CoinSignData csds[wc->request.withdraw.num_coins]; - enum TALER_ErrorCode ec_denomination_sign; - - memset (csds, - 0, - sizeof(csds)); - - /* Pick the chosen blinded coins */ - for (uint32_t i = 0; i<wc->request.withdraw.num_coins; i++) - { - csds[i].bp = &wc->request.planchets[i + offset]; - csds[i].h_denom_pub = &wc->request.denoms_h[i]; - } - - ec_denomination_sign = TEH_keys_denomination_batch_sign ( - wc->request.withdraw.num_coins, - csds, - false, - wc->request.withdraw.denom_sigs); - if (TALER_EC_NONE != ec_denomination_sign) - { - GNUNET_break (0); - SET_ERROR_WITH_FIELD (wc, - WITHDRAW_ERROR_DENOMINATION_SIGN, - ec_denomination_sign); - return; - } - - /* Save the hash value of the selected batch of coins */ - wc->request.withdraw.selected_h = - wc->request.kappa_planchets_h[wc->request.withdraw.noreveal_index]; - } - - /** - * For the denominations with cipher CS, calculate the R-values - * and save the choices we made now, as at a later point, the - * private keys for the denominations might now be available anymore - * to make the same choice again. - */ - if (0 < wc->request.withdraw.num_cs_r_values) - { - size_t num_cs_r_values = wc->request.withdraw.num_cs_r_values; - struct TEH_CsDeriveData cdds[num_cs_r_values]; - struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_values]; - - memset (nonces, - 0, - sizeof(nonces)); - wc->request.withdraw.cs_r_values - = GNUNET_new_array ( - num_cs_r_values, - struct GNUNET_CRYPTO_CSPublicRPairP); - wc->request.withdraw.cs_r_choices = 0; - - GNUNET_assert (! wc->request.withdraw.no_blinding_seed); - TALER_cs_derive_nonces_from_seed ( - &wc->request.withdraw.blinding_seed, - false, /* not for melt */ - num_cs_r_values, - wc->request.cs_indices, - nonces); - - for (size_t i = 0; i < num_cs_r_values; i++) - { - size_t idx = wc->request.cs_indices[i]; - - GNUNET_assert (idx < wc->request.withdraw.num_coins); - cdds[i].h_denom_pub = &wc->request.denoms_h[idx]; - cdds[i].nonce = &nonces[i]; - } - - /** - * Let the crypto helper generate the R-values and make the choices. - */ - if (TALER_EC_NONE != - TEH_keys_denomination_cs_batch_r_pub_simple ( - wc->request.withdraw.num_cs_r_values, - cdds, - false, - wc->request.withdraw.cs_r_values)) - { - GNUNET_break (0); - SET_ERROR (wc, - WITHDRAW_ERROR_CRYPTO_HELPER); - return; - } - - /* Now save the choices for the selected bits */ - for (size_t i = 0; i < num_cs_r_values; i++) - { - size_t idx = wc->request.cs_indices[i]; - - struct TALER_BlindedDenominationSignature *sig = - &wc->request.withdraw.denom_sigs[idx]; - uint8_t bit = sig->blinded_sig->details.blinded_cs_answer.b; - - wc->request.withdraw.cs_r_choices |= bit << i; - GNUNET_static_assert ( - TALER_MAX_COINS <= - sizeof(wc->request.withdraw.cs_r_choices) * 8); - } - } - wc->phase++; -} - - -/** - * Check the KYC result. - * - * @param wc context for request processing - */ -static void -phase_check_kyc_result (struct WithdrawContext *wc) -{ - /* return final positive response */ - if (! wc->kyc.ok) - { - SET_ERROR (wc, - WITHDRAW_ERROR_KYC_REQUIRED); - return; - } - wc->phase++; -} - - -/** - * Function called with the result of a legitimization - * check. - * - * @param cls closure - * @param lcr legitimization check result - */ -static void -withdraw_legi_cb ( - void *cls, - const struct TEH_LegitimizationCheckResult *lcr) -{ - struct WithdrawContext *wc = cls; - - wc->lch = NULL; - GNUNET_assert (WITHDRAW_PHASE_SUSPENDED == - wc->phase); - MHD_resume_connection (wc->rc->connection); - GNUNET_CONTAINER_DLL_remove (wc_head, - wc_tail, - wc); - TALER_MHD_daemon_trigger (); - if (NULL != lcr->response) - { - wc->error.details.legitimization_result.response = lcr->response; - wc->error.details.legitimization_result.http_status = lcr->http_status; - SET_ERROR (wc, - WITHDRAW_ERROR_LEGITIMIZATION_RESULT); - return; - } - wc->kyc = lcr->kyc; - wc->phase = WITHDRAW_PHASE_CHECK_KYC_RESULT; -} - - -/** - * 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 WithdrawContext - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -withdraw_amount_cb ( - void *cls, - struct GNUNET_TIME_Absolute limit, - TALER_EXCHANGEDB_KycAmountCallback cb, - void *cb_cls) -{ - struct WithdrawContext *wc = cls; - enum GNUNET_GenericReturnValue ret; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Signaling amount %s for KYC check during witdrawal\n", - TALER_amount2s (&wc->request.withdraw.amount_with_fee)); - - ret = cb (cb_cls, - &wc->request.withdraw.amount_with_fee, - wc->now.abs_time); - GNUNET_break (GNUNET_SYSERR != ret); - if (GNUNET_OK != ret) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - - qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( - TEH_plugin->cls, - &wc->h_normalized_payto, - limit, - cb, - cb_cls); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got %d additional transactions for this withdrawal and limit %llu\n", - qs, - (unsigned long long) limit.abs_value_us); - GNUNET_break (qs >= 0); - return qs; -} - - -/** - * Do legitimization check. - * - * @param wc operation context - */ -static void -phase_run_legi_check (struct WithdrawContext *wc) -{ - enum GNUNET_DB_QueryStatus qs; - struct TALER_FullPayto payto_uri; - struct TALER_FullPaytoHashP h_full_payto; - - /* Check if the money came from a wire transfer */ - qs = TEH_plugin->reserves_get_origin ( - TEH_plugin->cls, - &wc->request.withdraw.reserve_pub, - &h_full_payto, - &payto_uri); - if (qs < 0) - { - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_DB_FETCH_FAILED, - db_fetch_context, - "reserves_get_origin"); - return; - } - /* 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_NO_RESULTS == qs) - { - wc->phase = WITHDRAW_PHASE_PREPARE_TRANSACTION; - return; - } - TALER_full_payto_normalize_and_hash (payto_uri, - &wc->h_normalized_payto); - wc->lch = TEH_legitimization_check ( - &wc->rc->async_scope_id, - TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, - payto_uri, - &wc->h_normalized_payto, - NULL, /* no account pub: this is about the origin account */ - &withdraw_amount_cb, - wc, - &withdraw_legi_cb, - wc); - GNUNET_assert (NULL != wc->lch); - GNUNET_free (payto_uri.full_payto); - GNUNET_CONTAINER_DLL_insert (wc_head, - wc_tail, - wc); - MHD_suspend_connection (wc->rc->connection); - wc->phase = WITHDRAW_PHASE_SUSPENDED; -} - - -/** - * Check if the given denomination is still or already valid, has not been - * revoked and potentically supports age restriction. - * - * @param[in,out] wc context for the withdraw operation - * @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 denomination key found, might be NULL - * @return true when denomation was found and valid, - * false when denomination was not valid and the state machine was advanced - */ -static enum GNUNET_GenericReturnValue -find_denomination ( - struct WithdrawContext *wc, - struct TEH_KeyStateHandle *ksh, - const struct TALER_DenominationHashP *denom_h, - struct TEH_DenominationKey **pdk) -{ - struct TEH_DenominationKey *dk; - - *pdk = NULL; - dk = TEH_keys_denomination_by_hash_from_state ( - ksh, - denom_h, - NULL, - NULL); - if (NULL == dk) - { - SET_ERROR_WITH_FIELD (wc, - WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, - denom_h); - return false; - } - if (GNUNET_TIME_absolute_is_past ( - dk->meta.expire_withdraw.abs_time)) - { - SET_ERROR_WITH_FIELD (wc, - WITHDRAW_ERROR_DENOMINATION_EXPIRED, - denom_h); - return false; - } - if (GNUNET_TIME_absolute_is_future ( - dk->meta.start.abs_time)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_FIELD (wc, - WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - denom_h); - return false; - } - if (dk->recoup_possible) - { - SET_ERROR (wc, - WITHDRAW_ERROR_DENOMINATION_REVOKED); - return false; - } - /* In case of age withdraw, make sure that the denomination supports age restriction */ - if (wc->request.withdraw.age_proof_required) - { - if (0 == dk->denom_pub.age_mask.bits) - { - GNUNET_break_op (0); - SET_ERROR_WITH_FIELD (wc, - WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - denom_h); - return false; - } - } - *pdk = dk; - return true; -} - - -/** - * Check if the given array of hashes of denomination_keys - * a) belong to valid denominations - * b) those are marked as age restricted, if the request is age restricted - * c) calculate the total amount of the denominations including fees - * for withdraw. - * - * @param wc context of the age withdrawal to check keys for - */ -static void -phase_check_keys ( - struct WithdrawContext *wc) -{ - struct TEH_KeyStateHandle *ksh; - bool is_cs_denom[wc->request.withdraw.num_coins]; - - memset (is_cs_denom, - 0, - sizeof(is_cs_denom)); - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - GNUNET_break (0); - SET_ERROR (wc, - WITHDRAW_ERROR_KEYS_MISSING); - return; - } - wc->request.withdraw.denom_serials = - GNUNET_new_array (wc->request.withdraw.num_coins, - uint64_t); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &wc->request.amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &wc->request.fee)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &wc->request.withdraw.amount_with_fee)); - - for (unsigned int i = 0; i < wc->request.withdraw.num_coins; i++) - { - struct TEH_DenominationKey *dk; - - if (! find_denomination (wc, - ksh, - &wc->request.denoms_h[i], - &dk)) - return; - switch (dk->denom_pub.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_INVALID: - /* This should never happen (memory corruption?) */ - GNUNET_assert (0); - case GNUNET_CRYPTO_BSA_RSA: - /* nothing to do here */ - break; - case GNUNET_CRYPTO_BSA_CS: - if (wc->request.withdraw.no_blinding_seed) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_BLINDING_SEED_REQUIRED); - return; - } - wc->request.withdraw.num_cs_r_values++; - is_cs_denom[i] = true; - break; - } - - /* Ensure the ciphers from the planchets match the denominations'. */ - if (wc->request.withdraw.age_proof_required) - { - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - { - size_t off = k * wc->request.withdraw.num_coins; - - if (dk->denom_pub.bsign_pub_key->cipher != - wc->request.planchets[i + off].blinded_message->cipher) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_CIPHER_MISMATCH); - return; - } - } - } - else - { - if (dk->denom_pub.bsign_pub_key->cipher != - wc->request.planchets[i].blinded_message->cipher) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_CIPHER_MISMATCH); - return; - } - } - - /* Accumulate the values */ - if (0 > TALER_amount_add (&wc->request.amount, - &wc->request.amount, - &dk->meta.value)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_AMOUNT_OVERFLOW); - return; - } - - /* Accumulate the withdraw fees */ - if (0 > TALER_amount_add (&wc->request.fee, - &wc->request.fee, - &dk->meta.fees.withdraw)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_FEE_OVERFLOW); - return; - } - wc->request.withdraw.denom_serials[i] = dk->meta.serial; - } - - /* Save the hash of the batch of planchets */ - if (! wc->request.withdraw.age_proof_required) - { - TALER_wallet_blinded_planchets_hash ( - wc->request.withdraw.num_coins, - wc->request.planchets, - wc->request.denoms_h, - &wc->request.withdraw.planchets_h); - } - else - { - struct GNUNET_HashContext *ctx; - - /** - * The age-proof-required case is a bit more involved, - * because we need to calculate and remember kappa hashes - * for each batch of coins. - */ - ctx = GNUNET_CRYPTO_hash_context_start (); - GNUNET_assert (NULL != ctx); - - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - { - size_t off = k * wc->request.withdraw.num_coins; - - TALER_wallet_blinded_planchets_hash ( - wc->request.withdraw.num_coins, - &wc->request.planchets[off], - wc->request.denoms_h, - &wc->request.kappa_planchets_h[k]); - GNUNET_CRYPTO_hash_context_read ( - ctx, - &wc->request.kappa_planchets_h[k], - sizeof(wc->request.kappa_planchets_h[k])); - } - GNUNET_CRYPTO_hash_context_finish ( - ctx, - &wc->request.withdraw.planchets_h.hash); - } - - /* Save the total amount including fees */ - if (0 > TALER_amount_add ( - &wc->request.withdraw.amount_with_fee, - &wc->request.amount, - &wc->request.fee)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); - return; - } - - /* Save the indices of CS denominations */ - if (0 < wc->request.withdraw.num_cs_r_values) - { - size_t j = 0; - - wc->request.cs_indices = GNUNET_new_array ( - wc->request.withdraw.num_cs_r_values, - uint32_t); - - for (size_t i = 0; i < wc->request.withdraw.num_coins; i++) - { - if (is_cs_denom[i]) - wc->request.cs_indices[j++] = i; - } - } - - wc->phase++; -} - - -/** - * Check that the client signature authorizing the withdrawal is valid. - * - * @param[in,out] wc request context to check - */ -static void -phase_check_reserve_signature ( - struct WithdrawContext *wc) -{ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_withdraw_verify ( - &wc->request.amount, - &wc->request.fee, - &wc->request.withdraw.planchets_h, - wc->request.withdraw.no_blinding_seed - ? NULL - : &wc->request.withdraw.blinding_seed, - (wc->request.withdraw.age_proof_required) - ? &TEH_age_restriction_config.mask - : NULL, - (wc->request.withdraw.age_proof_required) - ? wc->request.withdraw.max_age - : 0, - &wc->request.withdraw.reserve_pub, - &wc->request.withdraw.reserve_sig)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID); - return; - } - wc->phase++; -} - - -/** - * Free data inside of @a wd, but not @a wd itself. - * - * @param[in] wd withdraw data to free - */ -static void -free_db_withdraw_data (struct TALER_EXCHANGEDB_Withdraw *wd) -{ - if (NULL != wd->denom_sigs) - { - for (unsigned int i = 0; i<wd->num_coins; i++) - TALER_blinded_denom_sig_free (&wd->denom_sigs[i]); - GNUNET_free (wd->denom_sigs); - } - GNUNET_free (wd->denom_serials); - GNUNET_free (wd->cs_r_values); -} - - -/** - * Cleanup routine for withdraw request. - * The function is called upon completion of the request - * that should clean up @a rh_ctx. Can be NULL. - * - * @param rc request context to clean up - */ -static void -clean_withdraw_rc (struct TEH_RequestContext *rc) -{ - struct WithdrawContext *wc = rc->rh_ctx; - - if (NULL != wc->lch) - { - TEH_legitimization_check_cancel (wc->lch); - wc->lch = NULL; - } - GNUNET_free (wc->request.denoms_h); - for (unsigned int i = 0; i<wc->request.num_planchets; i++) - TALER_blinded_planchet_free (&wc->request.planchets[i]); - GNUNET_free (wc->request.planchets); - free_db_withdraw_data (&wc->request.withdraw); - GNUNET_free (wc->request.cs_indices); - if (wc->request.is_idempotent) - free_db_withdraw_data (&wc->request.withdraw_idem); - if ( (WITHDRAW_ERROR_LEGITIMIZATION_RESULT == wc->error.code) && - (NULL != wc->error.details.legitimization_result.response) ) - { - MHD_destroy_response (wc->error.details.legitimization_result.response); - wc->error.details.legitimization_result.response = NULL; - } - GNUNET_free (wc); -} - - -/** - * Generates response for the withdraw request. - * - * @param wc withdraw operation context - */ -static void -phase_generate_reply_success (struct WithdrawContext *wc) -{ - struct TALER_EXCHANGEDB_Withdraw *db_obj; - - db_obj = wc->request.is_idempotent - ? &wc->request.withdraw_idem - : &wc->request.withdraw; - - if (wc->request.withdraw.age_proof_required) - { - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec_confirmation_sign; - - ec_confirmation_sign = - TALER_exchange_online_withdraw_age_confirmation_sign ( - &TEH_keys_exchange_sign_, - &db_obj->planchets_h, - db_obj->noreveal_index, - &pub, - &sig); - if (TALER_EC_NONE != ec_confirmation_sign) - { - SET_ERROR_WITH_FIELD (wc, - WITHDRAW_ERROR_CONFIRMATION_SIGN, - ec_confirmation_sign); - return; - } - - finish_loop (wc, - TALER_MHD_REPLY_JSON_PACK ( - wc->rc->connection, - MHD_HTTP_CREATED, - GNUNET_JSON_pack_uint64 ("noreveal_index", - db_obj->noreveal_index), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub))); - } - else /* not age restricted */ - { - json_t *sigs; - - sigs = json_array (); - GNUNET_assert (NULL != sigs); - for (unsigned int i = 0; i<db_obj->num_coins; i++) - { - GNUNET_assert ( - 0 == - json_array_append_new ( - sigs, - GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_denom_sig ( - NULL, - &db_obj->denom_sigs[i])))); - } - finish_loop (wc, - TALER_MHD_REPLY_JSON_PACK ( - wc->rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("ev_sigs", - sigs))); - } - - TEH_METRICS_withdraw_num_coins += db_obj->num_coins; -} - - -/** - * Reports an error, potentially with details. - * That is, it puts a error-type specific response into the MHD queue. - * It will do a idempotency check first, if needed for the error type. - * - * @param wc withdraw context - */ -static void -phase_generate_reply_error ( - struct WithdrawContext *wc) -{ - GNUNET_assert (WITHDRAW_PHASE_GENERATE_REPLY_ERROR == wc->phase); - if (IDEMPOTENCY_CHECK_REQUIRED (wc->error.code) && - withdraw_is_idempotent (wc)) - { - return; - } - - switch (wc->error.code) - { - case WITHDRAW_ERROR_NONE: - break; - case WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - wc->error.details.request_parameter_malformed)); - return; - case WITHDRAW_ERROR_KEYS_MISSING: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL)); - return; - case WITHDRAW_ERROR_DB_FETCH_FAILED: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - wc->error.details.db_fetch_context)); - return; - case WITHDRAW_ERROR_DB_INVARIANT_FAILURE: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL)); - return; - case WITHDRAW_ERROR_RESERVE_UNKNOWN: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - NULL)); - return; - case WITHDRAW_ERROR_DENOMINATION_SIGN: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - wc->error.details.ec_denomination_sign, - NULL)); - return; - case WITHDRAW_ERROR_KYC_REQUIRED: - finish_loop (wc, - TEH_RESPONSE_reply_kyc_required ( - wc->rc->connection, - &wc->h_normalized_payto, - &wc->kyc, - false)); - return; - case WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN: - GNUNET_break_op (0); - finish_loop (wc, - TEH_RESPONSE_reply_unknown_denom_pub_hash ( - wc->rc->connection, - wc->error.details.denom_h)); - return; - case WITHDRAW_ERROR_DENOMINATION_EXPIRED: - GNUNET_break_op (0); - finish_loop (wc, - TEH_RESPONSE_reply_expired_denom_pub_hash ( - wc->rc->connection, - wc->error.details.denom_h, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "WITHDRAW")); - return; - case WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: - finish_loop (wc, - TEH_RESPONSE_reply_expired_denom_pub_hash ( - wc->rc->connection, - wc->error.details.denom_h, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "WITHDRAW")); - return; - case WITHDRAW_ERROR_DENOMINATION_REVOKED: - GNUNET_break_op (0); - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - NULL)); - return; - case WITHDRAW_ERROR_CIPHER_MISMATCH: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL)); - return; - case WITHDRAW_ERROR_BLINDING_SEED_REQUIRED: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_GENERIC_PARAMETER_MISSING, - "blinding_seed")); - return; - case WITHDRAW_ERROR_CRYPTO_HELPER: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL)); - return; - case WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - "cipher")); - return; - case WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION: - { - char msg[256]; - - GNUNET_snprintf (msg, - sizeof(msg), - "denomination %s does not support age restriction", - GNUNET_h2s (&wc->error.details.denom_h->hash)); - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, - msg)); - return; - } - case WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Generating JSON response with code %d\n", - (int) TALER_EC_EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE); - finish_loop (wc, - TALER_MHD_REPLY_JSON_PACK ( - wc->rc->connection, - MHD_HTTP_CONFLICT, - TALER_MHD_PACK_EC ( - TALER_EC_EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE), - GNUNET_JSON_pack_uint64 ( - "allowed_maximum_age", - wc->error.details.maximum_age_too_large.max_allowed), - GNUNET_JSON_pack_uint64 ( - "reserve_birthday", - wc->error.details.maximum_age_too_large.birthday))); - return; - case WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED: - finish_loop (wc, - TEH_RESPONSE_reply_reserve_age_restriction_required ( - wc->rc->connection, - wc->error.details.age_restriction_required)); - return; - case WITHDRAW_ERROR_AMOUNT_OVERFLOW: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW, - "amount")); - return; - case WITHDRAW_ERROR_FEE_OVERFLOW: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW, - "fee")); - return; - case WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, - "amount+fee")); - return; - case WITHDRAW_ERROR_CONFIRMATION_SIGN: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - wc->error.details.ec_confirmation_sign, - NULL)); - return; - case WITHDRAW_ERROR_INSUFFICIENT_FUNDS: - finish_loop (wc, - TEH_RESPONSE_reply_reserve_insufficient_balance ( - wc->rc->connection, - TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, - &wc->error.details.insufficient_funds, - &wc->request.withdraw.amount_with_fee, - &wc->request.withdraw.reserve_pub)); - return; - case WITHDRAW_ERROR_IDEMPOTENT_PLANCHET: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET, - NULL)); - return; - case WITHDRAW_ERROR_NONCE_REUSE: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, - NULL)); - return; - case WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID: - finish_loop (wc, - TALER_MHD_reply_with_ec ( - wc->rc->connection, - TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, - NULL)); - return; - case WITHDRAW_ERROR_LEGITIMIZATION_RESULT: { - finish_loop ( - wc, - MHD_queue_response (wc->rc->connection, - wc->error.details.legitimization_result.http_status, - wc->error.details.legitimization_result.response)); - return; - } - } - GNUNET_break (0); - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "error phase without error")); -} - - -/** - * Initializes the new context for the incoming withdraw request - * - * @param[in,out] wc withdraw request context - * @param root json body of the request - */ -static void -withdraw_phase_parse ( - struct WithdrawContext *wc, - const json_t *root) -{ - const json_t *j_denoms_h; - const json_t *j_coin_evs; - const char *cipher; - bool no_max_age; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("cipher", - &cipher), - GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &wc->request.withdraw.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_mark_optional ( - GNUNET_JSON_spec_uint16 ("max_age", - &wc->request.withdraw.max_age), - &no_max_age), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("blinding_seed", - &wc->request.withdraw.blinding_seed), - &wc->request.withdraw.no_blinding_seed), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &wc->request.withdraw.reserve_sig), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (wc->rc->connection, - root, - spec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - wc->phase = (GNUNET_SYSERR == res) - ? WITHDRAW_PHASE_RETURN_NO - : WITHDRAW_PHASE_RETURN_YES; - return; - } - - /* For now, we only support cipher "ED25519" for signatures by the reserve */ - if (0 != strcmp ("ED25519", - cipher)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN, - reserve_cipher_unknown, - cipher); - return; - } - - wc->request.withdraw.age_proof_required = ! no_max_age; - - if (wc->request.withdraw.age_proof_required) - { - /* The age value MUST be on the beginning of an age group */ - if (wc->request.withdraw.max_age != - TALER_get_lowest_age (&TEH_age_restriction_config.mask, - wc->request.withdraw.max_age)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL ( - wc, - WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "max_age must be the lower edge of an age group"); - return; - } - } - - /* validate array size */ - { - size_t num_coins = json_array_size (j_denoms_h); - size_t array_size = json_array_size (j_coin_evs); - const char *error; - - GNUNET_static_assert ( - TALER_MAX_COINS < INT_MAX / TALER_CNC_KAPPA); - -#define BAIL_IF(cond, msg) \ - if ((cond)) { \ - GNUNET_break_op (0); \ - error = (msg); break; \ - } - - do { - BAIL_IF (0 == num_coins, - "denoms_h must not be empty") - - /** - * 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! - */ - BAIL_IF (num_coins > TALER_MAX_COINS, - "maximum number of coins that can be withdrawn has been exceeded") - - BAIL_IF ((! wc->request.withdraw.age_proof_required) && - (num_coins != array_size), - "denoms_h and coin_evs must be arrays of the same size") - - BAIL_IF (wc->request.withdraw.age_proof_required && - ((TALER_CNC_KAPPA * num_coins) != array_size), - "coin_evs must be an array of length " - TALER_CNC_KAPPA_STR - "*len(denoms_h)") - - wc->request.withdraw.num_coins = num_coins; - wc->request.num_planchets = array_size; - error = NULL; - - } while (0); -#undef BAIL_IF - - if (NULL != error) - { - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - error); - return; - } - } - /* extract the denomination hashes */ - { - size_t idx; - json_t *value; - - wc->request.denoms_h - = GNUNET_new_array (wc->request.withdraw.num_coins, - struct TALER_DenominationHashP); - - json_array_foreach (j_denoms_h, idx, value) { - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &wc->request.denoms_h[idx]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (wc->rc->connection, - value, - ispec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - wc->phase = (GNUNET_SYSERR == res) - ? WITHDRAW_PHASE_RETURN_NO - : WITHDRAW_PHASE_RETURN_YES; - return; - } - } - } - /* Parse the blinded coin envelopes */ - { - json_t *j_cev; - size_t idx; - - wc->request.planchets = - GNUNET_new_array (wc->request.num_planchets, - struct TALER_BlindedPlanchet); - json_array_foreach (j_coin_evs, idx, j_cev) - { - /* Now parse the individual envelopes and calculate the hash of - * the commitment along the way. */ - struct GNUNET_JSON_Specification kspec[] = { - TALER_JSON_spec_blinded_planchet (NULL, - &wc->request.planchets[idx]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (wc->rc->connection, - j_cev, - kspec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - wc->phase = (GNUNET_SYSERR == res) - ? WITHDRAW_PHASE_RETURN_NO - : WITHDRAW_PHASE_RETURN_YES; - return; - } - - /* Check for duplicate planchets. Technically a bug on - * the client side that is harmless for us, but still - * not allowed per protocol */ - for (size_t i = 0; i < idx; i++) - { - if (0 == - TALER_blinded_planchet_cmp ( - &wc->request.planchets[idx], - &wc->request.planchets[i])) - { - GNUNET_break_op (0); - SET_ERROR (wc, - WITHDRAW_ERROR_IDEMPOTENT_PLANCHET); - return; - } - } /* end duplicate check */ - } /* json_array_foreach over j_coin_evs */ - } /* scope of j_kappa_planchets, idx */ - wc->phase = WITHDRAW_PHASE_CHECK_KEYS; -} - - -MHD_RESULT -TEH_handler_withdraw ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]) -{ - struct WithdrawContext *wc = rc->rh_ctx; - - (void) args; - if (NULL == wc) - { - wc = GNUNET_new (struct WithdrawContext); - rc->rh_ctx = wc; - rc->rh_cleaner = &clean_withdraw_rc; - wc->rc = rc; - wc->now = GNUNET_TIME_timestamp_get (); - } - while (true) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "withdrawal%s processing in phase %d\n", - wc->request.withdraw.age_proof_required - ? " (with required age proof)" - : "", - wc->phase); - switch (wc->phase) - { - case WITHDRAW_PHASE_PARSE: - withdraw_phase_parse (wc, - root); - break; - case WITHDRAW_PHASE_CHECK_KEYS: - phase_check_keys (wc); - break; - case WITHDRAW_PHASE_CHECK_RESERVE_SIGNATURE: - phase_check_reserve_signature (wc); - break; - case WITHDRAW_PHASE_RUN_LEGI_CHECK: - phase_run_legi_check (wc); - break; - case WITHDRAW_PHASE_SUSPENDED: - return MHD_YES; - case WITHDRAW_PHASE_CHECK_KYC_RESULT: - phase_check_kyc_result (wc); - break; - case WITHDRAW_PHASE_PREPARE_TRANSACTION: - phase_prepare_transaction (wc); - break; - case WITHDRAW_PHASE_RUN_TRANSACTION: - phase_run_transaction (wc); - break; - case WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS: - phase_generate_reply_success (wc); - break; - case WITHDRAW_PHASE_GENERATE_REPLY_ERROR: - phase_generate_reply_error (wc); - break; - case WITHDRAW_PHASE_RETURN_YES: - return MHD_YES; - case WITHDRAW_PHASE_RETURN_NO: - return MHD_NO; - } - } -} diff --git a/src/exchange/taler-exchange-httpd_withdraw.h b/src/exchange/taler-exchange-httpd_withdraw.h @@ -1,61 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 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 <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_withdraw.h - * @brief Handle /withdraw requests - * @note This endpoint was introduced in v26 of the protocol. - * @author Özgür Kesim - */ -#ifndef TALER_EXCHANGE_HTTPD_WITHDRAW_H -#define TALER_EXCHANGE_HTTPD_WITHDRAW_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - -/** - * Resume suspended connections, we are shutting down. - */ -void -TEH_withdraw_cleanup (void); - -/** - * @brief Handle a "/withdraw" request. - * @note This endpoint was introduced in v24 of the protocol. - * - * Parses the batch of requested "denom_pub" which specifies - * the key/value of the respective coin to be withdrawn, - * and checks the signature "reserve_sig" for given "reserve_pub" - * makes this a valid withdrawal request from the specific reserve. - * If the "max_age" value is set in the request, - * it is considered a commitment to withdraw age restricted coins. - * If the request is valid, the response contains a noreveal_index - * which the client has to use for the subsequent call to /reveal-withdraw. - * If "max_age" value is not set, and the request is valid, the envelopes - * with the blinded coins "blinded_coin_evs" is processed - * and the client receives the blinded signatures as response. - * - * @param rc request context - * @param root uploaded JSON data - * @param args array of additional options, not used. - * @return MHD result code - */ -MHD_RESULT -TEH_handler_withdraw ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]); - -#endif diff --git a/src/include/taler/taler_exchangedb_plugin.h b/src/include/taler/taler_exchangedb_plugin.h @@ -1001,7 +1001,7 @@ struct TALER_EXCHANGEDB_Reserve /** * Meta data about a denomination public key. * If this is changed, you must also adjust - * taler-exchange-httpd_management_post_keys.c::denomination_meta_cmp(). + * taler-exchange-httpd-post-management-keys.c::denomination_meta_cmp(). */ struct TALER_EXCHANGEDB_DenominationKeyMetaData {