exchange_api_refund.c (14903B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2014-2023 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU 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 General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 TALER; see the file COPYING. If not, see 15 <http://www.gnu.org/licenses/> 16 */ 17 /** 18 * @file lib/exchange_api_refund.c 19 * @brief Implementation of the /refund request of the exchange's HTTP API 20 * @author Christian Grothoff 21 */ 22 #include "taler/platform.h" 23 #include <jansson.h> 24 #include <microhttpd.h> /* just for HTTP status codes */ 25 #include <gnunet/gnunet_util_lib.h> 26 #include <gnunet/gnunet_json_lib.h> 27 #include <gnunet/gnunet_curl_lib.h> 28 #include "taler/taler_json_lib.h" 29 #include "taler/taler_exchange_service.h" 30 #include "exchange_api_handle.h" 31 #include "taler/taler_signatures.h" 32 #include "exchange_api_curl_defaults.h" 33 34 35 /** 36 * @brief A Refund Handle 37 */ 38 struct TALER_EXCHANGE_RefundHandle 39 { 40 41 /** 42 * The keys of the exchange this request handle will use 43 */ 44 struct TALER_EXCHANGE_Keys *keys; 45 46 /** 47 * The url for this request. 48 */ 49 char *url; 50 51 /** 52 * Context for #TEH_curl_easy_post(). Keeps the data that must 53 * persist for Curl to make the upload. 54 */ 55 struct TALER_CURL_PostContext ctx; 56 57 /** 58 * Handle for the request. 59 */ 60 struct GNUNET_CURL_Job *job; 61 62 /** 63 * Function to call with the result. 64 */ 65 TALER_EXCHANGE_RefundCallback cb; 66 67 /** 68 * Closure for @a cb. 69 */ 70 void *cb_cls; 71 72 /** 73 * Hash over the proposal data to identify the contract 74 * which is being refunded. 75 */ 76 struct TALER_PrivateContractHashP h_contract_terms; 77 78 /** 79 * The coin's public key. This is the value that must have been 80 * signed (blindly) by the Exchange. 81 */ 82 struct TALER_CoinSpendPublicKeyP coin_pub; 83 84 /** 85 * The Merchant's public key. Allows the merchant to later refund 86 * the transaction or to inquire about the wire transfer identifier. 87 */ 88 struct TALER_MerchantPublicKeyP merchant; 89 90 /** 91 * Merchant-generated transaction ID for the refund. 92 */ 93 uint64_t rtransaction_id; 94 95 /** 96 * Amount to be refunded, including refund fee charged by the 97 * exchange to the customer. 98 */ 99 struct TALER_Amount refund_amount; 100 101 }; 102 103 104 /** 105 * Verify that the signature on the "200 OK" response 106 * from the exchange is valid. 107 * 108 * @param[in,out] rh refund handle (refund fee added) 109 * @param json json reply with the signature 110 * @param[out] exchange_pub set to the exchange's public key 111 * @param[out] exchange_sig set to the exchange's signature 112 * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not 113 */ 114 static enum GNUNET_GenericReturnValue 115 verify_refund_signature_ok (struct TALER_EXCHANGE_RefundHandle *rh, 116 const json_t *json, 117 struct TALER_ExchangePublicKeyP *exchange_pub, 118 struct TALER_ExchangeSignatureP *exchange_sig) 119 { 120 struct GNUNET_JSON_Specification spec[] = { 121 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 122 exchange_sig), 123 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 124 exchange_pub), 125 GNUNET_JSON_spec_end () 126 }; 127 128 if (GNUNET_OK != 129 GNUNET_JSON_parse (json, 130 spec, 131 NULL, NULL)) 132 { 133 GNUNET_break_op (0); 134 return GNUNET_SYSERR; 135 } 136 if (GNUNET_OK != 137 TALER_EXCHANGE_test_signing_key (rh->keys, 138 exchange_pub)) 139 { 140 GNUNET_break_op (0); 141 return GNUNET_SYSERR; 142 } 143 if (GNUNET_OK != 144 TALER_exchange_online_refund_confirmation_verify ( 145 &rh->h_contract_terms, 146 &rh->coin_pub, 147 &rh->merchant, 148 rh->rtransaction_id, 149 &rh->refund_amount, 150 exchange_pub, 151 exchange_sig)) 152 { 153 GNUNET_break_op (0); 154 return GNUNET_SYSERR; 155 } 156 return GNUNET_OK; 157 } 158 159 160 /** 161 * Verify that the information on the "412 Dependency Failed" response 162 * from the exchange is valid and indeed shows that there is a refund 163 * transaction ID reuse going on. 164 * 165 * @param[in,out] rh refund handle (refund fee added) 166 * @param json json reply with the signature 167 * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not 168 */ 169 static enum GNUNET_GenericReturnValue 170 verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh, 171 const json_t *json) 172 { 173 const json_t *h; 174 json_t *e; 175 struct GNUNET_JSON_Specification spec[] = { 176 GNUNET_JSON_spec_array_const ("history", 177 &h), 178 GNUNET_JSON_spec_end () 179 }; 180 181 if (GNUNET_OK != 182 GNUNET_JSON_parse (json, 183 spec, 184 NULL, NULL)) 185 { 186 GNUNET_break_op (0); 187 return GNUNET_SYSERR; 188 } 189 if (1 != json_array_size (h)) 190 { 191 GNUNET_break_op (0); 192 return GNUNET_SYSERR; 193 } 194 e = json_array_get (h, 0); 195 { 196 struct TALER_Amount amount; 197 const char *type; 198 struct TALER_MerchantSignatureP sig; 199 struct TALER_Amount refund_fee; 200 struct TALER_PrivateContractHashP h_contract_terms; 201 uint64_t rtransaction_id; 202 struct TALER_MerchantPublicKeyP merchant_pub; 203 struct GNUNET_JSON_Specification ispec[] = { 204 TALER_JSON_spec_amount_any ("amount", 205 &amount), 206 GNUNET_JSON_spec_string ("type", 207 &type), 208 TALER_JSON_spec_amount_any ("refund_fee", 209 &refund_fee), 210 GNUNET_JSON_spec_fixed_auto ("merchant_sig", 211 &sig), 212 GNUNET_JSON_spec_fixed_auto ("h_contract_terms", 213 &h_contract_terms), 214 GNUNET_JSON_spec_fixed_auto ("merchant_pub", 215 &merchant_pub), 216 GNUNET_JSON_spec_uint64 ("rtransaction_id", 217 &rtransaction_id), 218 GNUNET_JSON_spec_end () 219 }; 220 221 if (GNUNET_OK != 222 GNUNET_JSON_parse (e, 223 ispec, 224 NULL, NULL)) 225 { 226 GNUNET_break_op (0); 227 return GNUNET_SYSERR; 228 } 229 if (GNUNET_OK != 230 TALER_merchant_refund_verify (&rh->coin_pub, 231 &h_contract_terms, 232 rtransaction_id, 233 &amount, 234 &merchant_pub, 235 &sig)) 236 { 237 GNUNET_break_op (0); 238 return GNUNET_SYSERR; 239 } 240 if ( (rtransaction_id != rh->rtransaction_id) || 241 (0 != GNUNET_memcmp (&rh->h_contract_terms, 242 &h_contract_terms)) || 243 (0 != GNUNET_memcmp (&rh->merchant, 244 &merchant_pub)) || 245 (0 == TALER_amount_cmp (&rh->refund_amount, 246 &amount)) ) 247 { 248 GNUNET_break_op (0); 249 return GNUNET_SYSERR; 250 } 251 } 252 return GNUNET_OK; 253 } 254 255 256 /** 257 * Function called when we're done processing the 258 * HTTP /refund request. 259 * 260 * @param cls the `struct TALER_EXCHANGE_RefundHandle` 261 * @param response_code HTTP response code, 0 on error 262 * @param response parsed JSON result, NULL on error 263 */ 264 static void 265 handle_refund_finished (void *cls, 266 long response_code, 267 const void *response) 268 { 269 struct TALER_EXCHANGE_RefundHandle *rh = cls; 270 const json_t *j = response; 271 struct TALER_EXCHANGE_RefundResponse rr = { 272 .hr.reply = j, 273 .hr.http_status = (unsigned int) response_code 274 }; 275 276 rh->job = NULL; 277 switch (response_code) 278 { 279 case 0: 280 rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 281 break; 282 case MHD_HTTP_OK: 283 if (GNUNET_OK != 284 verify_refund_signature_ok (rh, 285 j, 286 &rr.details.ok.exchange_pub, 287 &rr.details.ok.exchange_sig)) 288 { 289 GNUNET_break_op (0); 290 rr.hr.http_status = 0; 291 rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE; 292 } 293 break; 294 case MHD_HTTP_BAD_REQUEST: 295 /* This should never happen, either us or the exchange is buggy 296 (or API version conflict); also can happen if the currency 297 differs (which we should obviously never support). 298 Just pass JSON reply to the application */ 299 rr.hr.ec = TALER_JSON_get_error_code (j); 300 rr.hr.hint = TALER_JSON_get_error_hint (j); 301 break; 302 case MHD_HTTP_FORBIDDEN: 303 /* Nothing really to verify, exchange says one of the signatures is 304 invalid; as we checked them, this should never happen, we 305 should pass the JSON reply to the application */ 306 rr.hr.ec = TALER_JSON_get_error_code (j); 307 rr.hr.hint = TALER_JSON_get_error_hint (j); 308 break; 309 case MHD_HTTP_NOT_FOUND: 310 /* Nothing really to verify, this should never 311 happen, we should pass the JSON reply to the application */ 312 rr.hr.ec = TALER_JSON_get_error_code (j); 313 rr.hr.hint = TALER_JSON_get_error_hint (j); 314 break; 315 case MHD_HTTP_CONFLICT: 316 /* Requested total refunds exceed deposited amount */ 317 rr.hr.ec = TALER_JSON_get_error_code (j); 318 rr.hr.hint = TALER_JSON_get_error_hint (j); 319 break; 320 case MHD_HTTP_GONE: 321 /* Kind of normal: the money was already sent to the merchant 322 (it was too late for the refund). */ 323 rr.hr.ec = TALER_JSON_get_error_code (j); 324 rr.hr.hint = TALER_JSON_get_error_hint (j); 325 break; 326 case MHD_HTTP_FAILED_DEPENDENCY: 327 rr.hr.ec = TALER_JSON_get_error_code (j); 328 rr.hr.hint = TALER_JSON_get_error_hint (j); 329 break; 330 case MHD_HTTP_PRECONDITION_FAILED: 331 if (GNUNET_OK != 332 verify_failed_dependency_ok (rh, 333 j)) 334 { 335 GNUNET_break (0); 336 rr.hr.http_status = 0; 337 rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE; 338 rr.hr.hint = "failed precondition proof returned by exchange is invalid"; 339 break; 340 } 341 /* Two different refund requests were made about the same deposit, but 342 carrying identical refund transaction ids. */ 343 rr.hr.ec = TALER_JSON_get_error_code (j); 344 rr.hr.hint = TALER_JSON_get_error_hint (j); 345 break; 346 case MHD_HTTP_INTERNAL_SERVER_ERROR: 347 /* Server had an internal issue; we should retry, but this API 348 leaves this to the application */ 349 rr.hr.ec = TALER_JSON_get_error_code (j); 350 rr.hr.hint = TALER_JSON_get_error_hint (j); 351 break; 352 default: 353 /* unexpected response code */ 354 GNUNET_break_op (0); 355 rr.hr.ec = TALER_JSON_get_error_code (j); 356 rr.hr.hint = TALER_JSON_get_error_hint (j); 357 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 358 "Unexpected response code %u/%d for exchange refund\n", 359 (unsigned int) response_code, 360 rr.hr.ec); 361 break; 362 } 363 rh->cb (rh->cb_cls, 364 &rr); 365 TALER_EXCHANGE_refund_cancel (rh); 366 } 367 368 369 struct TALER_EXCHANGE_RefundHandle * 370 TALER_EXCHANGE_refund ( 371 struct GNUNET_CURL_Context *ctx, 372 const char *url, 373 struct TALER_EXCHANGE_Keys *keys, 374 const struct TALER_Amount *amount, 375 const struct TALER_PrivateContractHashP *h_contract_terms, 376 const struct TALER_CoinSpendPublicKeyP *coin_pub, 377 uint64_t rtransaction_id, 378 const struct TALER_MerchantPrivateKeyP *merchant_priv, 379 TALER_EXCHANGE_RefundCallback cb, 380 void *cb_cls) 381 { 382 struct TALER_MerchantPublicKeyP merchant_pub; 383 struct TALER_MerchantSignatureP merchant_sig; 384 struct TALER_EXCHANGE_RefundHandle *rh; 385 json_t *refund_obj; 386 CURL *eh; 387 char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32]; 388 389 GNUNET_CRYPTO_eddsa_key_get_public (&merchant_priv->eddsa_priv, 390 &merchant_pub.eddsa_pub); 391 TALER_merchant_refund_sign (coin_pub, 392 h_contract_terms, 393 rtransaction_id, 394 amount, 395 merchant_priv, 396 &merchant_sig); 397 { 398 char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2]; 399 char *end; 400 401 end = GNUNET_STRINGS_data_to_string ( 402 coin_pub, 403 sizeof (struct TALER_CoinSpendPublicKeyP), 404 pub_str, 405 sizeof (pub_str)); 406 *end = '\0'; 407 GNUNET_snprintf (arg_str, 408 sizeof (arg_str), 409 "coins/%s/refund", 410 pub_str); 411 } 412 refund_obj = GNUNET_JSON_PACK ( 413 TALER_JSON_pack_amount ("refund_amount", 414 amount), 415 GNUNET_JSON_pack_data_auto ("h_contract_terms", 416 h_contract_terms), 417 GNUNET_JSON_pack_uint64 ("rtransaction_id", 418 rtransaction_id), 419 GNUNET_JSON_pack_data_auto ("merchant_pub", 420 &merchant_pub), 421 GNUNET_JSON_pack_data_auto ("merchant_sig", 422 &merchant_sig)); 423 rh = GNUNET_new (struct TALER_EXCHANGE_RefundHandle); 424 rh->cb = cb; 425 rh->cb_cls = cb_cls; 426 rh->url = TALER_url_join (url, 427 arg_str, 428 NULL); 429 if (NULL == rh->url) 430 { 431 json_decref (refund_obj); 432 GNUNET_free (rh); 433 return NULL; 434 } 435 rh->h_contract_terms = *h_contract_terms; 436 rh->coin_pub = *coin_pub; 437 rh->merchant = merchant_pub; 438 rh->rtransaction_id = rtransaction_id; 439 rh->refund_amount = *amount; 440 eh = TALER_EXCHANGE_curl_easy_get_ (rh->url); 441 if ( (NULL == eh) || 442 (GNUNET_OK != 443 TALER_curl_easy_post (&rh->ctx, 444 eh, 445 refund_obj)) ) 446 { 447 GNUNET_break (0); 448 if (NULL != eh) 449 curl_easy_cleanup (eh); 450 json_decref (refund_obj); 451 GNUNET_free (rh->url); 452 GNUNET_free (rh); 453 return NULL; 454 } 455 json_decref (refund_obj); 456 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 457 "URL for refund: `%s'\n", 458 rh->url); 459 rh->keys = TALER_EXCHANGE_keys_incref (keys); 460 rh->job = GNUNET_CURL_job_add2 (ctx, 461 eh, 462 rh->ctx.headers, 463 &handle_refund_finished, 464 rh); 465 return rh; 466 } 467 468 469 void 470 TALER_EXCHANGE_refund_cancel (struct TALER_EXCHANGE_RefundHandle *refund) 471 { 472 if (NULL != refund->job) 473 { 474 GNUNET_CURL_job_cancel (refund->job); 475 refund->job = NULL; 476 } 477 GNUNET_free (refund->url); 478 TALER_curl_easy_post_finished (&refund->ctx); 479 TALER_EXCHANGE_keys_decref (refund->keys); 480 GNUNET_free (refund); 481 } 482 483 484 /* end of exchange_api_refund.c */