diff options
-rw-r--r-- | src/exchange/taler-exchange-httpd_purses_create.c | 609 | ||||
-rw-r--r-- | src/include/taler_crypto_lib.h | 2 | ||||
-rw-r--r-- | src/include/taler_exchange_service.h | 23 | ||||
-rw-r--r-- | src/lib/exchange_api_purse_create_with_deposit.c | 12 |
4 files changed, 637 insertions, 9 deletions
diff --git a/src/exchange/taler-exchange-httpd_purses_create.c b/src/exchange/taler-exchange-httpd_purses_create.c new file mode 100644 index 000000000..ee822b259 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_purses_create.c @@ -0,0 +1,609 @@ +/* + 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 "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_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_purses_create.h" +#include "taler-exchange-httpd_responses.h" +#include "taler_exchangedb_lib.h" +#include "taler-exchange-httpd_keys.h" + + +/** + * Information about an individual coin being deposited. + */ +struct Coin +{ + /** + * Public information about the coin. + */ + struct TALER_CoinPublicInfo cpi; + + /** + * Signature affirming spending the coin. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Amount to be put into the purse from this coin. + */ + struct TALER_Amount amount; + + /** + * Deposit fee applicable for this coin. + */ + struct TALER_Amount deposit_fee; +}; + + +/** + * Closure for #create_transaction. + */ +struct PurseCreateContext +{ + /** + * 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; + + /** + * Our current time. + */ + struct GNUNET_TIME_Timestamp exchange_timestamp; + + /** + * Merge key for the purse. + */ + struct TALER_PurseMergePublicKeyP merge_pub; + + /** + * Contract decryption key for the purse. + */ + struct TALER_ContractDiffiePublicP contract_pub; + + /** + * Signature of the client affiming this request. + */ + struct TALER_PurseContractSignatureP purse_sig; + + /** + * Hash of the contract terms of the purse. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Array of coins being deposited. + */ + struct Coin *coins; + + /** + * Encrypted contract, can be NULL. + */ + void *econtract; + + /** + * Number of bytes in @e econtract. + */ + size_t econtract_size; + + /** + * Length of the @e coins array. + */ + unsigned int num_coins; + +}; + + +/** + * 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_create_success (struct MHD_Connection *connection, + const struct PurseCreateContext *pcc) +{ + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + // FIXME: define what exactly we sign over! + struct TALER_DepositConfirmationPS dc = { + .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_PURSE_CREATION), + .purpose.size = htonl (sizeof (dc)), + .h_contract_terms = pcc->h_contract_terms, + .purse_pub = *pcc->purse_pub + }; + enum TALER_ErrorCode ec; + + TALER_amount_hton (&dc.amount_without_fee, + &pcc->amount); + if (TALER_EC_NONE != + (ec = TEH_keys_exchange_sign (&dc, + &pub, + &sig))) + { + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + // GNUNET_JSON_pack_timestamp ("exchange_timestamp", + // pcc->exchange_timestamp), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub)); +} + + +/** + * 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; + bool balance_ok; + bool in_conflict; + + // 1) create purse + // 2) deposit all coins + // 3) if present, persist contract + // Also: nicely report errors... + qs = TEH_plugin->XXX (TEH_plugin->cls, ...); + 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"); + return qs; + } + if (in_conflict) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT, + &coin->pci.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &coin->pci.coin_pub); + 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] our HTTP connection + * @param[in,out] 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 Coin *coin, + const json_t *jcoin) +{ + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("amount", + TEH_currency, + &coin->amount), + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &coin->cpi.denom_pub_hash), + TALER_JSON_spec_denom_sig ("ub_sig", + &coin->cpi.denom_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &coin->cpi.h_age_commitment)), + &coin->cpi.no_age_commitment), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &coin->coin_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &coin->cpi.coin_pub), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + jcoin, + spec); + if (GNUNET_OK != res) + return res; + } + + if (GNUNET_OK != + TALER_wallet_purse_deposit_verify ("http://FIXME-URL", + &pcc->purse_pub, + &pcc->amount, + &coin->coin_pub, + &coin->csig)) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_UNAUTHORIZED, + TALER_EC_EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID, + NULL)) + ? GNUNET_NO : GNUNET_SYSERR; + } + /* check denomination exists and is valid */ + { + struct TEH_DenominationKey *dk; + MHD_RESULT mret; + + dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, + connection, + &mret); + if (NULL == dk) + { + GNUNET_JSON_parse_free (spec); + return (MHD_YES == mret) ? 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 ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "PURSE CREATE")) + ? 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 ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "PURSE CREATE")) + ? 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 ( + connection, + &deposit.coin.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "PURSE CREATE")) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (dk->denom_pub.cipher != deposit.coin.denom_sig.cipher) + { + /* denomination cipher and denomination signature cipher not the same */ + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)) + ? GNUNET_NO : GNUNET_SYSERR; + } + + coin->deposit_fee = dk->meta.fees.deposit; + /* check coin signature */ + switch (dk->denom_pub.cipher) + { + case TALER_DENOMINATION_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case TALER_DENOMINATION_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } + if (GNUNET_YES != + TALER_test_coin_valid (&deposit.coin, + &dk->denom_pub)) + { + TALER_LOG_WARNING ("Invalid coin passed for /deposit\n"); + GNUNET_JSON_parse_free (spec); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_UNAUTHORIZED, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)) + ? GNUNET_NO : GNUNET_SYSERR; + } + } + { + MHD_RESULT mhd_ret = MHD_NO; + enum GNUNET_DB_QueryStatus qs; + + /* make sure coin is 'known' in database */ + for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++) + { + qs = TEH_make_coin_known (&deposit.coin, + connection, + &pcc.known_coin_id, + &mhd_ret); + /* no transaction => no serialization failures should be possible */ + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "make_coin_known")) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (qs < 0) + return (MHD_YES == mhd_ret) ? GNUNET_NO : GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_purses_create (struct MHD_Connection *connection, + const struct + TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root) +{ + struct PurseCreateContext pcc = { + .purse_pub = purse_pub, + .exchange_timestamp = GNUNET_TIME_timestamp_get (); + }; + json_t *deposits; + json_t *deposit; + unsigned int idx; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("amount", + TEH_currency, + &pcc.amount), + GNUNET_JSON_spec_uint32 ("min_age", + &pcc.min_age), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_var_size ("econtract", + &pcc.econtract, + &pcc.ecotract_size)), + GNUNET_JSON_spec_fixed_auto ("contract_pub", + &pcc.contract_pub), + 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.h_contract_terms), + GNUNET_JSON_spec_json ("deposits", + &deposits), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &pcc.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 */ + } + } + + if (GNUNET_TIME_timestamp_cmp (pcc.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.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_FRESH_COINS) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_PARAMETER_MALFORMED, + "deposits"); + } + /* parse deposits */ + pcc.coins = GNUNET_new_array (struct Coin, + pcc.num_coins); + json_array_foreach (deposits, idx, deposit) + { + enum GNUNET_GenericReturnValue res; + struct Coin *coin = &pcc.coins[idx]; + + res = parse_coin (connection, + &pcc, + coin, + deposit); + if (GNUNET_OK != res) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (pcc.coins); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + } + + // FIXME: get purse_fee! + if (0 < TALER_amount_cmp (&purse_fee, + &pcc.deposit_total)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (pcc.coins); + 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.purse_expiration, + &pcc.h_contract_terms, + &pcc.merge_pub, + pcc.min_age, + &pcc.amount, + &pcc.purse_pub, + &pcc.purse_sig)) + { + TALER_LOG_WARNING ("Invalid signature on /purses/$PID/create request\n"); + GNUNET_JSON_parse_free (spec); + GNUNET_free (pcc.coins); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_UNAUTHORIZED, + TALER_EC_EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID, + NULL); + } + + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + 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 create", + TEH_MT_REQUEST_PURSE_CREATE, + &mhd_ret, + &create_transaction, + &pcc)) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (pcc.coins); + return mhd_ret; + } + } + + /* generate regular response */ + { + MHD_RESULT res; + + res = reply_create_success (connection, + &pcc); + GNUNET_free (pcc.coins); + GNUNET_JSON_parse_free (spec); + return res; + } +} + + +/* end of taler-exchange-httpd_purses_create.c */ diff --git a/src/include/taler_crypto_lib.h b/src/include/taler_crypto_lib.h index 179c62662..cc489e2c2 100644 --- a/src/include/taler_crypto_lib.h +++ b/src/include/taler_crypto_lib.h @@ -29,7 +29,7 @@ /** - * Maximum number of fresh coins we allow per refresh operation. + * Maximum number of coins we allow per operation. */ #define TALER_MAX_FRESH_COINS 256 diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h index 00c8c101f..20e7943fe 100644 --- a/src/include/taler_exchange_service.h +++ b/src/include/taler_exchange_service.h @@ -3985,10 +3985,16 @@ struct TALER_EXCHANGE_ContractGetResponse /** * Full HTTP response. */ - struct TALER_EXCHANGE_HttpResponse *hr; + struct TALER_EXCHANGE_HttpResponse hr; + /** + * Details depending on the HTTP status code. + */ union { + /** + * Information returned on #MHD_HTTP_OK. + */ struct { @@ -4000,13 +4006,13 @@ struct TALER_EXCHANGE_ContractGetResponse /** * This is a request for payment. */ - TALER_EXCHANGE_CONTRACT_PAYMENT_REQUEST, + TALER_EXCHANGE_CONTRACT_PAYMENT_REQUEST = 0, /** * This is a payment, the receiver needs to * accepts the terms. */ - TALER_EXCHANGE_CONTRACT_PAYMENT_OFFER + TALER_EXCHANGE_CONTRACT_PAYMENT_OFFER = 1 } type; /** @@ -4026,7 +4032,7 @@ struct TALER_EXCHANGE_ContractGetResponse } keys; /** - * Total value of the purse. + * Total value of the contract/purse. */ struct TALER_Amount amount; @@ -4046,6 +4052,7 @@ struct TALER_EXCHANGE_ContractGetResponse struct GNUNET_TIME_Timestamp purse_expiration; } success; + } details; }; @@ -4105,7 +4112,7 @@ struct TALER_EXCHANGE_PurseGetResponse /** * Full HTTP response. */ - struct TALER_EXCHANGE_HttpResponse *hr; + struct TALER_EXCHANGE_HttpResponse hr; /** * Details depending on the HTTP status. @@ -4327,7 +4334,7 @@ struct TALER_EXCHANGE_AccountMergeResponse /** * Full HTTP response. */ - struct TALER_EXCHANGE_HttpResponse *hr; + struct TALER_EXCHANGE_HttpResponse hr; /** * Details depending on the HTTP status. @@ -4411,7 +4418,7 @@ struct TALER_EXCHANGE_PurseCreateMergeResponse /** * Full HTTP response. */ - struct TALER_EXCHANGE_HttpResponse *hr; + struct TALER_EXCHANGE_HttpResponse hr; /** * Details depending on the HTTP status. @@ -4498,7 +4505,7 @@ struct TALER_EXCHANGE_PurseDepositResponse /** * Full HTTP response. */ - struct TALER_EXCHANGE_HttpResponse *hr; + struct TALER_EXCHANGE_HttpResponse hr; /** * Details depending on the HTTP status. diff --git a/src/lib/exchange_api_purse_create_with_deposit.c b/src/lib/exchange_api_purse_create_with_deposit.c index e39cb076f..5ce91e0e3 100644 --- a/src/lib/exchange_api_purse_create_with_deposit.c +++ b/src/lib/exchange_api_purse_create_with_deposit.c @@ -212,6 +212,7 @@ TALER_EXCHANGE_purse_create_with_deposit ( json_t *deposit_arr; CURL *eh; struct TALER_PurseMergePublicKeyP merge_pub; + struct TALER_PurseContractSignatureP purse_sig; struct TALER_PurseContractPublicKeyP purse_pub; struct TALER_ContractDiffiePublicP contract_pub; struct TALER_PrivateContractHashP h_contract_terms; @@ -335,6 +336,13 @@ TALER_EXCHANGE_purse_create_with_deposit ( jdeposit)); } GNUNET_free (url); + TALER_wallet_purse_create_sign (purse_expiration, + &h_contract_terms, + &merge_pub, + min_age, + &purse_value_after_fees, + purse_priv, + &purse_sig); { void *econtract = NULL; size_t econtract_size = 0; @@ -357,10 +365,14 @@ TALER_EXCHANGE_purse_create_with_deposit ( econtract_size)), GNUNET_JSON_pack_data_auto ("contract_pub", &contract_pub), + GNUNET_JSON_pack_data_auto ("purse_sig", + &purse_sig), GNUNET_JSON_pack_data_auto ("merge_pub", &merge_pub), GNUNET_JSON_pack_data_auto ("h_contract_terms", &h_contract_terms), + GNUNET_JSON_pack_timestamp ("purse_expiration", + purse_expiration), GNUNET_JSON_pack_array_steal ("deposits", deposit_arr)); GNUNET_free (econtract); |