taler-exchange-httpd_recoup.c (14106B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2017-2022 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file taler-exchange-httpd_recoup.c 18 * @brief Handle /recoup requests; parses the POST and JSON and 19 * verifies the coin signature before handing things off 20 * to the database. 21 * @author Christian Grothoff 22 */ 23 #include "taler/platform.h" 24 #include <gnunet/gnunet_util_lib.h> 25 #include <gnunet/gnunet_json_lib.h> 26 #include <jansson.h> 27 #include <microhttpd.h> 28 #include <pthread.h> 29 #include "taler/taler_json_lib.h" 30 #include "taler/taler_mhd_lib.h" 31 #include "taler-exchange-httpd_db.h" 32 #include "taler-exchange-httpd_recoup.h" 33 #include "taler-exchange-httpd_responses.h" 34 #include "taler-exchange-httpd_keys.h" 35 #include "taler/taler_exchangedb_lib.h" 36 37 /** 38 * Closure for #recoup_transaction. 39 */ 40 struct RecoupContext 41 { 42 /** 43 * Hash identifying the withdraw request. 44 */ 45 struct TALER_BlindedCoinHashP h_coin_ev; 46 47 /** 48 * Set by #recoup_transaction() to the reserve that will 49 * receive the recoup, if #refreshed is #GNUNET_NO. 50 */ 51 struct TALER_ReservePublicKeyP reserve_pub; 52 53 /** 54 * Details about the coin. 55 */ 56 const struct TALER_CoinPublicInfo *coin; 57 58 /** 59 * Key used to blind the coin. 60 */ 61 const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; 62 63 /** 64 * Signature of the coin requesting recoup. 65 */ 66 const struct TALER_CoinSpendSignatureP *coin_sig; 67 68 /** 69 * Unique ID of the withdraw operation in the withdraw table. 70 */ 71 uint64_t withdraw_serial_id; 72 73 /** 74 * Unique ID of the coin in the known_coins table. 75 */ 76 uint64_t known_coin_id; 77 78 /** 79 * Set by #recoup_transaction to the timestamp when the recoup 80 * was accepted. 81 */ 82 struct GNUNET_TIME_Timestamp now; 83 84 }; 85 86 87 /** 88 * Execute a "recoup". The validity of the coin and signature have 89 * already been checked. The database must now check that the coin is 90 * not (double) spent, and execute the transaction. 91 * 92 * IF it returns a non-error code, the transaction logic MUST 93 * NOT queue a MHD response. IF it returns an hard error, the 94 * transaction logic MUST queue a MHD response and set @a mhd_ret. IF 95 * it returns the soft error code, the function MAY be called again to 96 * retry and MUST not queue a MHD response. 97 * 98 * @param cls the `struct RecoupContext *` 99 * @param connection MHD request which triggered the transaction 100 * @param[out] mhd_ret set to MHD response status for @a connection, 101 * if transaction failed (!) 102 * @return transaction status code 103 */ 104 static enum GNUNET_DB_QueryStatus 105 recoup_transaction (void *cls, 106 struct MHD_Connection *connection, 107 MHD_RESULT *mhd_ret) 108 { 109 struct RecoupContext *pc = cls; 110 enum GNUNET_DB_QueryStatus qs; 111 bool recoup_ok; 112 bool internal_failure; 113 114 /* Finally, store new refund data */ 115 pc->now = GNUNET_TIME_timestamp_get (); 116 qs = TEH_plugin->do_recoup (TEH_plugin->cls, 117 &pc->reserve_pub, 118 pc->withdraw_serial_id, 119 pc->coin_bks, 120 &pc->coin->coin_pub, 121 pc->known_coin_id, 122 pc->coin_sig, 123 &pc->now, 124 &recoup_ok, 125 &internal_failure); 126 if (0 > qs) 127 { 128 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 129 *mhd_ret = TALER_MHD_reply_with_error ( 130 connection, 131 MHD_HTTP_INTERNAL_SERVER_ERROR, 132 TALER_EC_GENERIC_DB_FETCH_FAILED, 133 "do_recoup"); 134 return qs; 135 } 136 137 if (internal_failure) 138 { 139 GNUNET_break (0); 140 *mhd_ret = TALER_MHD_reply_with_error ( 141 connection, 142 MHD_HTTP_INTERNAL_SERVER_ERROR, 143 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 144 "do_recoup"); 145 return GNUNET_DB_STATUS_HARD_ERROR; 146 } 147 if (! recoup_ok) 148 { 149 *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( 150 connection, 151 TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, 152 &pc->coin->denom_pub_hash, 153 &pc->coin->coin_pub); 154 return GNUNET_DB_STATUS_HARD_ERROR; 155 } 156 return qs; 157 } 158 159 160 /** 161 * We have parsed the JSON information about the recoup request. Do 162 * some basic sanity checks (especially that the signature on the 163 * request and coin is valid) and then execute the recoup operation. 164 * Note that we need the DB to check the fee structure, so this is not 165 * done here but during the recoup_transaction(). 166 * 167 * @param connection the MHD connection to handle 168 * @param coin information about the coin 169 * @param exchange_vals values contributed by the exchange 170 * during withdrawal 171 * @param coin_bks blinding data of the coin (to be checked) 172 * @param h_planchets The hash of the commitment of the original withdraw request 173 * @param nonce coin's nonce if CS is used 174 * @param coin_sig signature of the coin 175 * @return MHD result code 176 */ 177 static MHD_RESULT 178 verify_and_execute_recoup ( 179 struct MHD_Connection *connection, 180 const struct TALER_CoinPublicInfo *coin, 181 const struct TALER_ExchangeBlindingValues *exchange_vals, 182 const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, 183 const struct TALER_HashBlindedPlanchetsP *h_planchets, 184 const union GNUNET_CRYPTO_BlindSessionNonce *nonce, 185 const struct TALER_CoinSpendSignatureP *coin_sig) 186 { 187 struct RecoupContext pc; 188 const struct TEH_DenominationKey *dk; 189 MHD_RESULT mret; 190 191 /* check denomination exists and is in recoup mode */ 192 dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, 193 connection, 194 &mret); 195 if (NULL == dk) 196 return mret; 197 if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) 198 { 199 /* This denomination is past the expiration time for recoup */ 200 return TEH_RESPONSE_reply_expired_denom_pub_hash ( 201 connection, 202 &coin->denom_pub_hash, 203 TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, 204 "RECOUP"); 205 } 206 if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) 207 { 208 /* This denomination is not yet valid */ 209 return TEH_RESPONSE_reply_expired_denom_pub_hash ( 210 connection, 211 &coin->denom_pub_hash, 212 TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, 213 "RECOUP"); 214 } 215 if (! dk->recoup_possible) 216 { 217 /* This denomination is not eligible for recoup */ 218 return TEH_RESPONSE_reply_expired_denom_pub_hash ( 219 connection, 220 &coin->denom_pub_hash, 221 TALER_EC_EXCHANGE_RECOUP_NOT_ELIGIBLE, 222 "RECOUP"); 223 } 224 225 /* check denomination signature */ 226 switch (dk->denom_pub.bsign_pub_key->cipher) 227 { 228 case GNUNET_CRYPTO_BSA_RSA: 229 TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; 230 break; 231 case GNUNET_CRYPTO_BSA_CS: 232 TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; 233 break; 234 default: 235 break; 236 } 237 if (GNUNET_YES != 238 TALER_test_coin_valid (coin, 239 &dk->denom_pub)) 240 { 241 GNUNET_break_op (0); 242 return TALER_MHD_reply_with_error ( 243 connection, 244 MHD_HTTP_FORBIDDEN, 245 TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, 246 NULL); 247 } 248 249 /* check recoup request signature */ 250 TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; 251 if (GNUNET_OK != 252 TALER_wallet_recoup_verify (&coin->denom_pub_hash, 253 coin_bks, 254 &coin->coin_pub, 255 coin_sig)) 256 { 257 GNUNET_break_op (0); 258 return TALER_MHD_reply_with_error ( 259 connection, 260 MHD_HTTP_FORBIDDEN, 261 TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID, 262 NULL); 263 } 264 265 /* re-compute client-side blinding so we can 266 (a bit later) check that this coin was indeed 267 signed by us. */ 268 { 269 struct TALER_CoinPubHashP c_hash; 270 struct TALER_BlindedPlanchet blinded_planchet; 271 272 if (GNUNET_OK != 273 TALER_denom_blind (&dk->denom_pub, 274 coin_bks, 275 nonce, 276 &coin->h_age_commitment, 277 &coin->coin_pub, 278 exchange_vals, 279 &c_hash, 280 &blinded_planchet)) 281 { 282 GNUNET_break (0); 283 return TALER_MHD_reply_with_error ( 284 connection, 285 MHD_HTTP_INTERNAL_SERVER_ERROR, 286 TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, 287 NULL); 288 } 289 TALER_coin_ev_hash (&blinded_planchet, 290 &coin->denom_pub_hash, 291 &pc.h_coin_ev); 292 TALER_blinded_planchet_free (&blinded_planchet); 293 } 294 295 pc.coin_sig = coin_sig; 296 pc.coin_bks = coin_bks; 297 pc.coin = coin; 298 299 { 300 MHD_RESULT mhd_ret = MHD_NO; 301 enum GNUNET_DB_QueryStatus qs; 302 303 /* make sure coin is 'known' in database */ 304 qs = TEH_make_coin_known (coin, 305 connection, 306 &pc.known_coin_id, 307 &mhd_ret); 308 /* no transaction => no serialization failures should be possible */ 309 GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); 310 if (qs < 0) 311 return mhd_ret; 312 } 313 314 { 315 enum GNUNET_DB_QueryStatus qs; 316 317 qs = TEH_plugin->get_reserve_by_h_planchets ( 318 TEH_plugin->cls, 319 h_planchets, 320 &pc.reserve_pub, 321 &pc.withdraw_serial_id); 322 if (0 > qs) 323 { 324 GNUNET_break (0); 325 return TALER_MHD_reply_with_error ( 326 connection, 327 MHD_HTTP_INTERNAL_SERVER_ERROR, 328 TALER_EC_GENERIC_DB_FETCH_FAILED, 329 "get_reserve_by_commitment"); 330 } 331 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 332 { 333 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 334 "Recoup requested for unknown envelope %s\n", 335 GNUNET_h2s (&pc.h_coin_ev.hash)); 336 return TALER_MHD_reply_with_error ( 337 connection, 338 MHD_HTTP_NOT_FOUND, 339 TALER_EC_EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND, 340 NULL); 341 } 342 } 343 344 /* Perform actual recoup transaction */ 345 { 346 MHD_RESULT mhd_ret; 347 348 if (GNUNET_OK != 349 TEH_DB_run_transaction (connection, 350 "run recoup", 351 TEH_MT_REQUEST_OTHER, 352 &mhd_ret, 353 &recoup_transaction, 354 &pc)) 355 return mhd_ret; 356 } 357 /* Recoup succeeded, return result */ 358 return TALER_MHD_REPLY_JSON_PACK (connection, 359 MHD_HTTP_OK, 360 GNUNET_JSON_pack_data_auto ( 361 "reserve_pub", 362 &pc.reserve_pub)); 363 } 364 365 366 /** 367 * Handle a "/coins/$COIN_PUB/recoup" request. Parses the JSON, and, if 368 * successful, passes the JSON data to #verify_and_execute_recoup() to further 369 * check the details of the operation specified. If everything checks out, 370 * this will ultimately lead to the refund being executed, or rejected. 371 * 372 * @param connection the MHD connection to handle 373 * @param coin_pub public key of the coin 374 * @param root uploaded JSON data 375 * @return MHD result code 376 */ 377 MHD_RESULT 378 TEH_handler_recoup (struct MHD_Connection *connection, 379 const struct TALER_CoinSpendPublicKeyP *coin_pub, 380 const json_t *root) 381 { 382 enum GNUNET_GenericReturnValue ret; 383 struct TALER_CoinPublicInfo coin; 384 union GNUNET_CRYPTO_BlindingSecretP coin_bks; 385 struct TALER_CoinSpendSignatureP coin_sig; 386 struct TALER_ExchangeBlindingValues exchange_vals; 387 struct TALER_HashBlindedPlanchetsP h_planchets; 388 union GNUNET_CRYPTO_BlindSessionNonce nonce; 389 bool no_nonce; 390 struct GNUNET_JSON_Specification spec[] = { 391 GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", 392 &coin.denom_pub_hash), 393 TALER_JSON_spec_denom_sig ("denom_sig", 394 &coin.denom_sig), 395 GNUNET_JSON_spec_fixed_auto ("h_planchets", 396 &h_planchets), 397 TALER_JSON_spec_exchange_blinding_values ("ewv", 398 &exchange_vals), 399 GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", 400 &coin_bks), 401 GNUNET_JSON_spec_fixed_auto ("coin_sig", 402 &coin_sig), 403 GNUNET_JSON_spec_mark_optional ( 404 GNUNET_JSON_spec_fixed_auto ("h_age_commitment", 405 &coin.h_age_commitment), 406 &coin.no_age_commitment), 407 GNUNET_JSON_spec_mark_optional ( 408 GNUNET_JSON_spec_fixed_auto ("nonce", 409 &nonce), 410 &no_nonce), 411 GNUNET_JSON_spec_end () 412 }; 413 414 memset (&coin, 415 0, 416 sizeof (coin)); 417 coin.coin_pub = *coin_pub; 418 ret = TALER_MHD_parse_json_data (connection, 419 root, 420 spec); 421 if (GNUNET_SYSERR == ret) 422 return MHD_NO; /* hard failure */ 423 if (GNUNET_NO == ret) 424 return MHD_YES; /* failure */ 425 { 426 MHD_RESULT res; 427 428 res = verify_and_execute_recoup (connection, 429 &coin, 430 &exchange_vals, 431 &coin_bks, 432 &h_planchets, 433 no_nonce 434 ? NULL 435 : &nonce, 436 &coin_sig); 437 GNUNET_JSON_parse_free (spec); 438 return res; 439 } 440 } 441 442 443 /* end of taler-exchange-httpd_recoup.c */