exchange_api_post-recoup-refresh.c (15244B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2017-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 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 lib/exchange_api_post-recoup-refresh.c 18 * @brief Implementation of the /recoup-refresh request of the exchange's HTTP API 19 * @author Christian Grothoff 20 * @author Özgür Kesim 21 */ 22 #include <jansson.h> 23 #include <microhttpd.h> /* just for HTTP status codes */ 24 #include <gnunet/gnunet_util_lib.h> 25 #include <gnunet/gnunet_json_lib.h> 26 #include <gnunet/gnunet_curl_lib.h> 27 #include "taler/taler_json_lib.h" 28 #include "exchange_api_common.h" 29 #include "exchange_api_handle.h" 30 #include "taler/taler_signatures.h" 31 #include "exchange_api_curl_defaults.h" 32 33 34 /** 35 * @brief A /recoup-refresh handle 36 */ 37 struct TALER_EXCHANGE_PostRecoupRefreshHandle 38 { 39 40 /** 41 * The base URL for this request. 42 */ 43 char *base_url; 44 45 /** 46 * The full URL for this request, set during _start. 47 */ 48 char *url; 49 50 /** 51 * Minor context that holds body and headers. 52 */ 53 struct TALER_CURL_PostContext post_ctx; 54 55 /** 56 * Handle for the request. 57 */ 58 struct GNUNET_CURL_Job *job; 59 60 /** 61 * Function to call with the result. 62 */ 63 TALER_EXCHANGE_PostRecoupRefreshCallback cb; 64 65 /** 66 * Closure for @a cb. 67 */ 68 TALER_EXCHANGE_POST_RECOUP_REFRESH_RESULT_CLOSURE *cb_cls; 69 70 /** 71 * Reference to the execution context. 72 */ 73 struct GNUNET_CURL_Context *ctx; 74 75 /** 76 * The keys of the exchange this request handle will use. 77 */ 78 struct TALER_EXCHANGE_Keys *keys; 79 80 /** 81 * Old coin the coins were refreshed from. 82 */ 83 struct TALER_CoinSpendPublicKeyP old_coin_pub; 84 85 /** 86 * Commitment of the refresh operation. 87 */ 88 struct TALER_RefreshCommitmentP rc; 89 90 /** 91 * Public keys of the recouped coins, in request order. 92 */ 93 struct TALER_CoinSpendPublicKeyP *recouped_pubs; 94 95 /** 96 * Denomination and age commitment of the recouped coins, 97 * parallel to @e recouped_pubs. 98 */ 99 struct TALER_EXCHANGE_RecoupedCoinInfo_ *recouped_infos; 100 101 /** 102 * Number of entries in @e recouped_pubs. 103 */ 104 size_t num_recouped; 105 106 /** 107 * Pre-built request body. 108 */ 109 json_t *body; 110 111 }; 112 113 114 /** 115 * Parse and verify a successful response. If it is valid, 116 * call the callback. 117 * 118 * @param prrh request handle 119 * @param json json reply with the signature 120 * @return #GNUNET_OK if the response is valid and we called the callback; 121 * #GNUNET_SYSERR if not (callback must still be called) 122 */ 123 static enum GNUNET_GenericReturnValue 124 process_ok_response ( 125 const struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh, 126 const json_t *json) 127 { 128 struct TALER_EXCHANGE_PostRecoupRefreshResponse rr = { 129 .hr.reply = json, 130 .hr.http_status = MHD_HTTP_OK 131 }; 132 const json_t *j_recoups; 133 struct TALER_RecoupedCoin *recoups; 134 struct GNUNET_HashCode h_recoups; 135 struct GNUNET_JSON_Specification spec[] = { 136 GNUNET_JSON_spec_fixed_auto ("old_coin_pub", 137 &rr.details.ok.old_coin_pub), 138 GNUNET_JSON_spec_fixed_auto ("rc", 139 &rr.details.ok.rc), 140 GNUNET_JSON_spec_timestamp ("timestamp", 141 &rr.details.ok.timestamp), 142 TALER_JSON_spec_amount_any ("total_amount", 143 &rr.details.ok.total_amount), 144 GNUNET_JSON_spec_array_const ("recoups", 145 &j_recoups), 146 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 147 &rr.details.ok.exchange_sig), 148 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 149 &rr.details.ok.exchange_pub), 150 GNUNET_JSON_spec_end () 151 }; 152 153 if (GNUNET_OK != 154 GNUNET_JSON_parse (json, 155 spec, 156 NULL, NULL)) 157 { 158 GNUNET_break_op (0); 159 return GNUNET_SYSERR; 160 } 161 if ( (0 != 162 GNUNET_memcmp (&rr.details.ok.old_coin_pub, 163 &prrh->old_coin_pub)) || 164 (0 != 165 GNUNET_memcmp (&rr.details.ok.rc, 166 &prrh->rc)) ) 167 { 168 GNUNET_break_op (0); 169 return GNUNET_SYSERR; 170 } 171 if (GNUNET_OK != 172 TALER_EXCHANGE_test_signing_key (prrh->keys, 173 &rr.details.ok.exchange_pub)) 174 { 175 GNUNET_break_op (0); 176 return GNUNET_SYSERR; 177 } 178 if (GNUNET_OK != 179 TALER_EXCHANGE_parse_recoups_ (j_recoups, 180 prrh->num_recouped, 181 prrh->recouped_pubs, 182 &rr.details.ok.total_amount, 183 &recoups, 184 &h_recoups)) 185 { 186 GNUNET_break_op (0); 187 return GNUNET_SYSERR; 188 } 189 if (GNUNET_OK != 190 TALER_exchange_online_confirm_recoup_refresh_batch_verify ( 191 rr.details.ok.timestamp, 192 &rr.details.ok.old_coin_pub, 193 &rr.details.ok.rc, 194 &rr.details.ok.total_amount, 195 &h_recoups, 196 &rr.details.ok.exchange_pub, 197 &rr.details.ok.exchange_sig)) 198 { 199 GNUNET_break_op (0); 200 GNUNET_free (recoups); 201 return GNUNET_SYSERR; 202 } 203 rr.details.ok.num_recoups = prrh->num_recouped; 204 rr.details.ok.recoups = recoups; 205 prrh->cb (prrh->cb_cls, 206 &rr); 207 GNUNET_free (recoups); 208 return GNUNET_OK; 209 } 210 211 212 /** 213 * Find the recouped coin @a coin_pub and how we used it. 214 * 215 * @param cls a `struct TALER_EXCHANGE_PostRecoupRefreshHandle *` 216 * @param coin_pub public key of the coin named in the conflict reply 217 * @param[out] h_denom_pub set to the hash of the denomination we used 218 * @param[out] h_age_commitment set to the age commitment hash we used, or NULL 219 * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours 220 */ 221 static enum GNUNET_GenericReturnValue 222 recoup_coin_lookup (void *cls, 223 const struct TALER_CoinSpendPublicKeyP *coin_pub, 224 const struct TALER_DenominationHashP **h_denom_pub, 225 const struct TALER_AgeCommitmentHashP **h_age_commitment) 226 { 227 struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh = cls; 228 229 for (size_t i = 0; i < prrh->num_recouped; i++) 230 { 231 if (0 != GNUNET_memcmp (coin_pub, 232 &prrh->recouped_pubs[i])) 233 continue; 234 *h_denom_pub = &prrh->recouped_infos[i].h_denom_pub; 235 *h_age_commitment = prrh->recouped_infos[i].have_age 236 ? &prrh->recouped_infos[i].h_age_commitment 237 : NULL; 238 return GNUNET_OK; 239 } 240 return GNUNET_NO; 241 } 242 243 244 /** 245 * Function called when we're done processing the 246 * HTTP /recoup-refresh request. 247 * 248 * @param cls the `struct TALER_EXCHANGE_PostRecoupRefreshHandle` 249 * @param response_code HTTP response code, 0 on error 250 * @param response parsed JSON result, NULL on error 251 */ 252 static void 253 handle_recoup_refresh_finished (void *cls, 254 long response_code, 255 const void *response) 256 { 257 struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh = cls; 258 const json_t *j = response; 259 struct TALER_EXCHANGE_PostRecoupRefreshResponse rr = { 260 .hr.reply = j, 261 .hr.http_status = (unsigned int) response_code 262 }; 263 264 prrh->job = NULL; 265 switch (response_code) 266 { 267 case 0: 268 rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 269 break; 270 case MHD_HTTP_OK: 271 if (GNUNET_OK != 272 process_ok_response (prrh, 273 j)) 274 { 275 GNUNET_break_op (0); 276 rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 277 rr.hr.http_status = 0; 278 break; 279 } 280 TALER_EXCHANGE_post_recoup_refresh_cancel (prrh); 281 return; 282 case MHD_HTTP_BAD_REQUEST: 283 /* This should never happen, either us or the exchange is buggy 284 (or API version conflict); just pass JSON reply to the application */ 285 rr.hr.ec = TALER_JSON_get_error_code (j); 286 rr.hr.hint = TALER_JSON_get_error_hint (j); 287 break; 288 case MHD_HTTP_FORBIDDEN: 289 /* Nothing really to verify, exchange says one of the signatures is 290 invalid; as we checked them, this should never happen, we 291 should pass the JSON reply to the application */ 292 rr.hr.ec = TALER_JSON_get_error_code (j); 293 rr.hr.hint = TALER_JSON_get_error_hint (j); 294 break; 295 case MHD_HTTP_NOT_FOUND: 296 /* Exchange does not know the denomination or the refresh 297 operation; pass the JSON reply to the application */ 298 rr.hr.ec = TALER_JSON_get_error_code (j); 299 rr.hr.hint = TALER_JSON_get_error_hint (j); 300 break; 301 case MHD_HTTP_CONFLICT: 302 rr.hr.ec = TALER_JSON_get_error_code (j); 303 rr.hr.hint = TALER_JSON_get_error_hint (j); 304 switch (rr.hr.ec) 305 { 306 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: 307 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: 308 if (GNUNET_OK != 309 TALER_EXCHANGE_check_coin_conflict_ (prrh->keys, 310 rr.hr.ec, 311 j, 312 &recoup_coin_lookup, 313 prrh, 314 &rr.details.conflict)) 315 { 316 GNUNET_break_op (0); 317 rr.hr.http_status = 0; 318 rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 319 } 320 break; 321 default: 322 /* Commitment mismatch, denomination mismatch or a coin without 323 residual value; pass the JSON reply to the application */ 324 break; 325 } 326 break; 327 case MHD_HTTP_GONE: 328 /* Denomination not eligible for recoup (not revoked, or expired) */ 329 rr.hr.ec = TALER_JSON_get_error_code (j); 330 rr.hr.hint = TALER_JSON_get_error_hint (j); 331 break; 332 case MHD_HTTP_PRECONDITION_FAILED: 333 /* Denomination not yet valid */ 334 rr.hr.ec = TALER_JSON_get_error_code (j); 335 rr.hr.hint = TALER_JSON_get_error_hint (j); 336 break; 337 case MHD_HTTP_INTERNAL_SERVER_ERROR: 338 /* Server had an internal issue; we should retry, but this API 339 leaves this to the application */ 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_BAD_GATEWAY: 344 case MHD_HTTP_SERVICE_UNAVAILABLE: 345 /* Exchange is (temporarily) unable to sign; retry later */ 346 rr.hr.ec = TALER_JSON_get_error_code (j); 347 rr.hr.hint = TALER_JSON_get_error_hint (j); 348 break; 349 default: 350 /* unexpected response code */ 351 rr.hr.ec = TALER_JSON_get_error_code (j); 352 rr.hr.hint = TALER_JSON_get_error_hint (j); 353 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 354 "Unexpected response code %u/%d for exchange recoup-refresh\n", 355 (unsigned int) response_code, 356 (int) rr.hr.ec); 357 GNUNET_break (0); 358 break; 359 } 360 prrh->cb (prrh->cb_cls, 361 &rr); 362 TALER_EXCHANGE_free_coin_conflict_ (&rr.hr, 363 &rr.details.conflict); 364 TALER_EXCHANGE_post_recoup_refresh_cancel (prrh); 365 } 366 367 368 struct TALER_EXCHANGE_PostRecoupRefreshHandle * 369 TALER_EXCHANGE_post_recoup_refresh_create ( 370 struct GNUNET_CURL_Context *ctx, 371 const char *url, 372 struct TALER_EXCHANGE_Keys *keys, 373 const struct TALER_CoinSpendPublicKeyP *old_coin_pub, 374 const struct TALER_RefreshCommitmentP *rc, 375 const struct TALER_BlindingMasterSeedP *blinding_seed, 376 size_t num_coins, 377 const struct TALER_EXCHANGE_RecoupCoin coins[static num_coins]) 378 { 379 struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh; 380 json_t *coin_data; 381 382 prrh = GNUNET_new (struct TALER_EXCHANGE_PostRecoupRefreshHandle); 383 prrh->recouped_pubs = GNUNET_new_array (num_coins, 384 struct TALER_CoinSpendPublicKeyP); 385 prrh->recouped_infos = GNUNET_new_array ( 386 num_coins, 387 struct TALER_EXCHANGE_RecoupedCoinInfo_); 388 coin_data = TALER_EXCHANGE_recoup_coin_data_ (num_coins, 389 coins, 390 blinding_seed, 391 true, /* for melt */ 392 prrh->recouped_pubs, 393 prrh->recouped_infos, 394 &prrh->num_recouped); 395 if (NULL == coin_data) 396 { 397 GNUNET_break (0); 398 GNUNET_free (prrh->recouped_pubs); 399 GNUNET_free (prrh->recouped_infos); 400 GNUNET_free (prrh); 401 return NULL; 402 } 403 prrh->ctx = ctx; 404 prrh->base_url = GNUNET_strdup (url); 405 prrh->keys = TALER_EXCHANGE_keys_incref (keys); 406 prrh->old_coin_pub = *old_coin_pub; 407 prrh->rc = *rc; 408 prrh->body = GNUNET_JSON_PACK ( 409 GNUNET_JSON_pack_data_auto ("old_coin_pub", 410 old_coin_pub), 411 GNUNET_JSON_pack_data_auto ("rc", 412 rc), 413 GNUNET_JSON_pack_array_steal ("coin_data", 414 coin_data)); 415 return prrh; 416 } 417 418 419 enum TALER_ErrorCode 420 TALER_EXCHANGE_post_recoup_refresh_start ( 421 struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh, 422 TALER_EXCHANGE_PostRecoupRefreshCallback cb, 423 TALER_EXCHANGE_POST_RECOUP_REFRESH_RESULT_CLOSURE *cb_cls) 424 { 425 CURL *eh; 426 427 prrh->cb = cb; 428 prrh->cb_cls = cb_cls; 429 prrh->url = TALER_url_join (prrh->base_url, 430 "recoup-refresh", 431 NULL); 432 if (NULL == prrh->url) 433 { 434 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 435 "Could not construct request URL.\n"); 436 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 437 } 438 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 439 "URL for recoup-refresh: `%s'\n", 440 prrh->url); 441 eh = TALER_EXCHANGE_curl_easy_get_ (prrh->url); 442 if ( (NULL == eh) || 443 (GNUNET_OK != 444 TALER_curl_easy_post (&prrh->post_ctx, 445 eh, 446 prrh->body)) ) 447 { 448 GNUNET_break (0); 449 if (NULL != eh) 450 curl_easy_cleanup (eh); 451 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 452 } 453 prrh->job = GNUNET_CURL_job_add2 (prrh->ctx, 454 eh, 455 prrh->post_ctx.headers, 456 &handle_recoup_refresh_finished, 457 prrh); 458 if (NULL == prrh->job) 459 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 460 return TALER_EC_NONE; 461 } 462 463 464 void 465 TALER_EXCHANGE_post_recoup_refresh_cancel ( 466 struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh) 467 { 468 if (NULL != prrh->job) 469 { 470 GNUNET_CURL_job_cancel (prrh->job); 471 prrh->job = NULL; 472 } 473 TALER_curl_easy_post_finished (&prrh->post_ctx); 474 GNUNET_free (prrh->url); 475 GNUNET_free (prrh->base_url); 476 GNUNET_free (prrh->recouped_pubs); 477 GNUNET_free (prrh->recouped_infos); 478 json_decref (prrh->body); 479 TALER_EXCHANGE_keys_decref (prrh->keys); 480 GNUNET_free (prrh); 481 } 482 483 484 /* end of exchange_api_post-recoup-refresh.c */