exchange_api_post-reserves-RESERVE_PUB-open.c (18083B)
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-reserves-RESERVE_PUB-open.c 19 * @brief Implementation of the POST /reserves/$RESERVE_PUB/open requests 20 * @author Christian Grothoff 21 */ 22 #include <jansson.h> 23 #include <microhttpd.h> /* just for HTTP open 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 * Information we keep per coin to validate the reply. 36 */ 37 struct CoinData 38 { 39 /** 40 * Public key of the coin. 41 */ 42 struct TALER_CoinSpendPublicKeyP coin_pub; 43 44 /** 45 * Signature by the coin. 46 */ 47 struct TALER_CoinSpendSignatureP coin_sig; 48 49 /** 50 * The hash of the denomination's public key 51 */ 52 struct TALER_DenominationHashP h_denom_pub; 53 54 /** 55 * Hash of the age commitment of the coin, if @e have_age. 56 */ 57 struct TALER_AgeCommitmentHashP h_age_commitment; 58 59 /** 60 * True if the coin has an age commitment. 61 */ 62 bool have_age; 63 64 /** 65 * How much did this coin contribute. 66 */ 67 struct TALER_Amount contribution; 68 }; 69 70 71 /** 72 * @brief A POST /reserves/$RID/open Handle 73 */ 74 struct TALER_EXCHANGE_PostReservesOpenHandle 75 { 76 77 /** 78 * Reference to the execution context. 79 */ 80 struct GNUNET_CURL_Context *ctx; 81 82 /** 83 * Base URL of the exchange. 84 */ 85 char *base_url; 86 87 /** 88 * The url for this request, set during _start. 89 */ 90 char *url; 91 92 /** 93 * Context for #TEH_curl_easy_post(). Keeps the data that must 94 * persist for Curl to make the upload. 95 */ 96 struct TALER_CURL_PostContext post_ctx; 97 98 /** 99 * Handle for the request. 100 */ 101 struct GNUNET_CURL_Job *job; 102 103 /** 104 * Function to call with the result. 105 */ 106 TALER_EXCHANGE_PostReservesOpenCallback cb; 107 108 /** 109 * Closure for @a cb. 110 */ 111 TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls; 112 113 /** 114 * The keys of the exchange this request handle will use 115 */ 116 struct TALER_EXCHANGE_Keys *keys; 117 118 /** 119 * Public key of the reserve we are querying. 120 */ 121 struct TALER_ReservePublicKeyP reserve_pub; 122 123 /** 124 * Information we keep per coin to validate the reply. 125 */ 126 struct CoinData *coins; 127 128 /** 129 * Length of the @e coins array. 130 */ 131 unsigned int num_coins; 132 133 /** 134 * Pre-built JSON body for the request. 135 */ 136 json_t *body; 137 138 }; 139 140 141 /** 142 * We received an #MHD_HTTP_OK open code. Handle the JSON 143 * response. 144 * 145 * @param proh handle of the request 146 * @param j JSON response 147 * @return #GNUNET_OK on success 148 */ 149 static enum GNUNET_GenericReturnValue 150 handle_reserves_open_ok (struct TALER_EXCHANGE_PostReservesOpenHandle *proh, 151 const json_t *j) 152 { 153 struct TALER_EXCHANGE_PostReservesOpenResponse rs = { 154 .hr.reply = j, 155 .hr.http_status = MHD_HTTP_OK, 156 }; 157 struct GNUNET_JSON_Specification spec[] = { 158 TALER_JSON_spec_amount_any ("open_cost", 159 &rs.details.ok.open_cost), 160 GNUNET_JSON_spec_timestamp ("reserve_expiration", 161 &rs.details.ok.expiration_time), 162 GNUNET_JSON_spec_end () 163 }; 164 165 if (GNUNET_OK != 166 GNUNET_JSON_parse (j, 167 spec, 168 NULL, 169 NULL)) 170 { 171 GNUNET_break_op (0); 172 return GNUNET_SYSERR; 173 } 174 proh->cb (proh->cb_cls, 175 &rs); 176 proh->cb = NULL; 177 GNUNET_JSON_parse_free (spec); 178 return GNUNET_OK; 179 } 180 181 182 /** 183 * We received an #MHD_HTTP_PAYMENT_REQUIRED open code. Handle the JSON 184 * response. 185 * 186 * @param proh handle of the request 187 * @param j JSON response 188 * @return #GNUNET_OK on success 189 */ 190 static enum GNUNET_GenericReturnValue 191 handle_reserves_open_pr (struct TALER_EXCHANGE_PostReservesOpenHandle *proh, 192 const json_t *j) 193 { 194 struct TALER_EXCHANGE_PostReservesOpenResponse rs = { 195 .hr.reply = j, 196 .hr.http_status = MHD_HTTP_PAYMENT_REQUIRED, 197 }; 198 struct GNUNET_JSON_Specification spec[] = { 199 TALER_JSON_spec_amount_any ("open_cost", 200 &rs.details.payment_required.open_cost), 201 GNUNET_JSON_spec_timestamp ("reserve_expiration", 202 &rs.details.payment_required.expiration_time), 203 GNUNET_JSON_spec_end () 204 }; 205 206 if (GNUNET_OK != 207 GNUNET_JSON_parse (j, 208 spec, 209 NULL, 210 NULL)) 211 { 212 GNUNET_break_op (0); 213 return GNUNET_SYSERR; 214 } 215 proh->cb (proh->cb_cls, 216 &rs); 217 proh->cb = NULL; 218 GNUNET_JSON_parse_free (spec); 219 return GNUNET_OK; 220 } 221 222 223 /** 224 * Find the coin @a coin_pub paid into the reserve and how we used it. 225 * 226 * @param cls a `struct TALER_EXCHANGE_PostReservesOpenHandle *` 227 * @param coin_pub public key of the coin named in the conflict reply 228 * @param[out] h_denom_pub set to the hash of the denomination we used 229 * @param[out] h_age_commitment set to the age commitment hash we used, or NULL 230 * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours 231 */ 232 static enum GNUNET_GenericReturnValue 233 reserve_open_coin_lookup (void *cls, 234 const struct TALER_CoinSpendPublicKeyP *coin_pub, 235 const struct TALER_DenominationHashP **h_denom_pub, 236 const struct TALER_AgeCommitmentHashP ** 237 h_age_commitment) 238 { 239 struct TALER_EXCHANGE_PostReservesOpenHandle *proh = cls; 240 241 for (unsigned int i = 0; i < proh->num_coins; i++) 242 { 243 const struct CoinData *cd = &proh->coins[i]; 244 245 if (0 != GNUNET_memcmp (coin_pub, 246 &cd->coin_pub)) 247 continue; 248 *h_denom_pub = &cd->h_denom_pub; 249 *h_age_commitment = cd->have_age 250 ? &cd->h_age_commitment 251 : NULL; 252 return GNUNET_OK; 253 } 254 return GNUNET_NO; 255 } 256 257 258 /** 259 * Function called when we're done processing the 260 * HTTP /reserves/$RID/open request. 261 * 262 * @param cls the `struct TALER_EXCHANGE_PostReservesOpenHandle` 263 * @param response_code HTTP response code, 0 on error 264 * @param response parsed JSON result, NULL on error 265 */ 266 static void 267 handle_reserves_open_finished (void *cls, 268 long response_code, 269 const void *response) 270 { 271 struct TALER_EXCHANGE_PostReservesOpenHandle *proh = cls; 272 const json_t *j = response; 273 struct TALER_EXCHANGE_PostReservesOpenResponse rs = { 274 .hr.reply = j, 275 .hr.http_status = (unsigned int) response_code 276 }; 277 278 proh->job = NULL; 279 switch (response_code) 280 { 281 case 0: 282 rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 283 break; 284 case MHD_HTTP_OK: 285 if (GNUNET_OK != 286 handle_reserves_open_ok (proh, 287 j)) 288 { 289 GNUNET_break_op (0); 290 rs.hr.http_status = 0; 291 rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 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); just pass JSON reply to the application */ 297 GNUNET_break (0); 298 json_dumpf (j, 299 stderr, 300 JSON_INDENT (2)); 301 rs.hr.ec = TALER_JSON_get_error_code (j); 302 rs.hr.hint = TALER_JSON_get_error_hint (j); 303 break; 304 case MHD_HTTP_PAYMENT_REQUIRED: 305 if (GNUNET_OK != 306 handle_reserves_open_pr (proh, 307 j)) 308 { 309 GNUNET_break_op (0); 310 rs.hr.http_status = 0; 311 rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 312 } 313 break; 314 case MHD_HTTP_FORBIDDEN: 315 /* This should never happen, either us or the exchange is buggy 316 (or API version conflict); just pass JSON reply to the application */ 317 GNUNET_break (0); 318 rs.hr.ec = TALER_JSON_get_error_code (j); 319 rs.hr.hint = TALER_JSON_get_error_hint (j); 320 break; 321 case MHD_HTTP_NOT_FOUND: 322 /* Nothing really to verify, this should never 323 happen, we should pass the JSON reply to the application */ 324 rs.hr.ec = TALER_JSON_get_error_code (j); 325 rs.hr.hint = TALER_JSON_get_error_hint (j); 326 break; 327 case MHD_HTTP_CONFLICT: 328 { 329 const struct CoinData *cd = NULL; 330 struct GNUNET_JSON_Specification spec[] = { 331 GNUNET_JSON_spec_fixed_auto ("coin_pub", 332 &rs.details.conflict.coin_pub), 333 GNUNET_JSON_spec_end () 334 }; 335 336 if (GNUNET_OK != 337 GNUNET_JSON_parse (j, 338 spec, 339 NULL, 340 NULL)) 341 { 342 GNUNET_break_op (0); 343 rs.hr.http_status = 0; 344 rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 345 break; 346 } 347 for (unsigned int i = 0; i < proh->num_coins; i++) 348 { 349 const struct CoinData *cdi = &proh->coins[i]; 350 351 if (0 == GNUNET_memcmp (&rs.details.conflict.coin_pub, 352 &cdi->coin_pub)) 353 { 354 cd = cdi; 355 break; 356 } 357 } 358 if (NULL == cd) 359 { 360 GNUNET_break_op (0); 361 rs.hr.http_status = 0; 362 rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 363 break; 364 } 365 rs.hr.ec = TALER_JSON_get_error_code (j); 366 rs.hr.hint = TALER_JSON_get_error_hint (j); 367 switch (rs.hr.ec) 368 { 369 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: 370 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: 371 if (GNUNET_OK != 372 TALER_EXCHANGE_check_coin_conflict_ (proh->keys, 373 rs.hr.ec, 374 j, 375 &reserve_open_coin_lookup, 376 proh, 377 &rs.details.conflict. 378 coin_conflict)) 379 { 380 GNUNET_break_op (0); 381 rs.hr.http_status = 0; 382 rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 383 } 384 break; 385 default: 386 /* insufficient funds: proof is the coin history, checked by 387 the application via GET /coins/$COIN_PUB/history */ 388 break; 389 } 390 break; 391 } 392 case MHD_HTTP_INTERNAL_SERVER_ERROR: 393 /* Server had an internal issue; we should retry, but this API 394 leaves this to the application */ 395 rs.hr.ec = TALER_JSON_get_error_code (j); 396 rs.hr.hint = TALER_JSON_get_error_hint (j); 397 break; 398 default: 399 /* unexpected response code */ 400 GNUNET_break_op (0); 401 rs.hr.ec = TALER_JSON_get_error_code (j); 402 rs.hr.hint = TALER_JSON_get_error_hint (j); 403 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 404 "Unexpected response code %u/%d for reserves open\n", 405 (unsigned int) response_code, 406 (int) rs.hr.ec); 407 break; 408 } 409 if (NULL != proh->cb) 410 { 411 proh->cb (proh->cb_cls, 412 &rs); 413 proh->cb = NULL; 414 } 415 TALER_EXCHANGE_free_coin_conflict_ (&rs.hr, 416 &rs.details.conflict.coin_conflict); 417 TALER_EXCHANGE_post_reserves_open_cancel (proh); 418 } 419 420 421 struct TALER_EXCHANGE_PostReservesOpenHandle * 422 TALER_EXCHANGE_post_reserves_open_create ( 423 struct GNUNET_CURL_Context *ctx, 424 const char *url, 425 struct TALER_EXCHANGE_Keys *keys, 426 const struct TALER_ReservePrivateKeyP *reserve_priv, 427 const struct TALER_Amount *reserve_contribution, 428 unsigned int coin_payments_length, 429 const struct TALER_EXCHANGE_PurseDeposit coin_payments[ 430 static coin_payments_length], 431 struct GNUNET_TIME_Timestamp expiration_time, 432 uint32_t min_purses) 433 { 434 struct TALER_EXCHANGE_PostReservesOpenHandle *proh; 435 struct GNUNET_TIME_Timestamp ts; 436 struct TALER_ReserveSignatureP reserve_sig; 437 json_t *cpa; 438 439 proh = GNUNET_new (struct TALER_EXCHANGE_PostReservesOpenHandle); 440 proh->ctx = ctx; 441 proh->base_url = GNUNET_strdup (url); 442 proh->keys = TALER_EXCHANGE_keys_incref (keys); 443 ts = GNUNET_TIME_timestamp_get (); 444 GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, 445 &proh->reserve_pub.eddsa_pub); 446 TALER_wallet_reserve_open_sign (reserve_contribution, 447 ts, 448 expiration_time, 449 min_purses, 450 reserve_priv, 451 &reserve_sig); 452 proh->coins = GNUNET_new_array (coin_payments_length, 453 struct CoinData); 454 proh->num_coins = coin_payments_length; 455 cpa = json_array (); 456 GNUNET_assert (NULL != cpa); 457 for (unsigned int i = 0; i < coin_payments_length; i++) 458 { 459 const struct TALER_EXCHANGE_PurseDeposit *pd = &coin_payments[i]; 460 const struct TALER_AgeCommitmentProof *acp = pd->age_commitment_proof; 461 struct TALER_AgeCommitmentHashP ahac = { 0 }; 462 struct TALER_AgeCommitmentHashP *achp = NULL; 463 struct CoinData *cd = &proh->coins[i]; 464 json_t *cp; 465 466 cd->contribution = pd->amount; 467 cd->h_denom_pub = pd->h_denom_pub; 468 if (NULL != acp) 469 { 470 TALER_age_commitment_hash (&acp->commitment, 471 &ahac); 472 achp = &ahac; 473 cd->h_age_commitment = ahac; 474 cd->have_age = true; 475 } 476 TALER_wallet_reserve_open_deposit_sign (&pd->amount, 477 &pd->h_denom_pub, 478 achp, 479 &reserve_sig, 480 &pd->coin_priv, 481 &cd->coin_sig); 482 GNUNET_CRYPTO_eddsa_key_get_public (&pd->coin_priv.eddsa_priv, 483 &cd->coin_pub.eddsa_pub); 484 485 cp = GNUNET_JSON_PACK ( 486 GNUNET_JSON_pack_allow_null ( 487 GNUNET_JSON_pack_data_auto ("h_age_commitment", 488 achp)), 489 TALER_JSON_pack_amount ("amount", 490 &pd->amount), 491 GNUNET_JSON_pack_data_auto ("denom_pub_hash", 492 &pd->h_denom_pub), 493 TALER_JSON_pack_denom_sig ("ub_sig", 494 &pd->denom_sig), 495 GNUNET_JSON_pack_data_auto ("coin_pub", 496 &cd->coin_pub), 497 GNUNET_JSON_pack_data_auto ("coin_sig", 498 &cd->coin_sig)); 499 GNUNET_assert (0 == 500 json_array_append_new (cpa, 501 cp)); 502 } 503 proh->body = GNUNET_JSON_PACK ( 504 GNUNET_JSON_pack_timestamp ("request_timestamp", 505 ts), 506 GNUNET_JSON_pack_timestamp ("reserve_expiration", 507 expiration_time), 508 GNUNET_JSON_pack_array_steal ("payments", 509 cpa), 510 TALER_JSON_pack_amount ("reserve_payment", 511 reserve_contribution), 512 GNUNET_JSON_pack_uint64 ("purse_limit", 513 min_purses), 514 GNUNET_JSON_pack_data_auto ("reserve_sig", 515 &reserve_sig)); 516 if (NULL == proh->body) 517 { 518 GNUNET_break (0); 519 GNUNET_free (proh->coins); 520 GNUNET_free (proh->base_url); 521 TALER_EXCHANGE_keys_decref (proh->keys); 522 GNUNET_free (proh); 523 return NULL; 524 } 525 return proh; 526 } 527 528 529 enum TALER_ErrorCode 530 TALER_EXCHANGE_post_reserves_open_start ( 531 struct TALER_EXCHANGE_PostReservesOpenHandle *proh, 532 TALER_EXCHANGE_PostReservesOpenCallback cb, 533 TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls) 534 { 535 CURL *eh; 536 char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; 537 538 proh->cb = cb; 539 proh->cb_cls = cb_cls; 540 { 541 char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; 542 char *end; 543 544 end = GNUNET_STRINGS_data_to_string ( 545 &proh->reserve_pub, 546 sizeof (proh->reserve_pub), 547 pub_str, 548 sizeof (pub_str)); 549 *end = '\0'; 550 GNUNET_snprintf (arg_str, 551 sizeof (arg_str), 552 "reserves/%s/open", 553 pub_str); 554 } 555 proh->url = TALER_url_join (proh->base_url, 556 arg_str, 557 NULL); 558 if (NULL == proh->url) 559 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 560 eh = TALER_EXCHANGE_curl_easy_get_ (proh->url); 561 if ( (NULL == eh) || 562 (GNUNET_OK != 563 TALER_curl_easy_post (&proh->post_ctx, 564 eh, 565 proh->body)) ) 566 { 567 GNUNET_break (0); 568 if (NULL != eh) 569 curl_easy_cleanup (eh); 570 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 571 } 572 proh->job = GNUNET_CURL_job_add2 (proh->ctx, 573 eh, 574 proh->post_ctx.headers, 575 &handle_reserves_open_finished, 576 proh); 577 if (NULL == proh->job) 578 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 579 return TALER_EC_NONE; 580 } 581 582 583 void 584 TALER_EXCHANGE_post_reserves_open_cancel ( 585 struct TALER_EXCHANGE_PostReservesOpenHandle *proh) 586 { 587 if (NULL != proh->job) 588 { 589 GNUNET_CURL_job_cancel (proh->job); 590 proh->job = NULL; 591 } 592 TALER_curl_easy_post_finished (&proh->post_ctx); 593 json_decref (proh->body); 594 GNUNET_free (proh->coins); 595 GNUNET_free (proh->url); 596 GNUNET_free (proh->base_url); 597 TALER_EXCHANGE_keys_decref (proh->keys); 598 GNUNET_free (proh); 599 } 600 601 602 /* end of exchange_api_post-reserves-RESERVE_PUB-open.c */