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