exchange_api_post-reserves-RESERVE_PUB-open.c (16896B)
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 "taler/platform.h" 23 #include <jansson.h> 24 #include <microhttpd.h> /* just for HTTP open 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_exchange_service.h" 29 #include "taler/taler_json_lib.h" 30 #include "exchange_api_common.h" 31 #include "exchange_api_handle.h" 32 #include "taler/taler_signatures.h" 33 #include "exchange_api_curl_defaults.h" 34 35 36 /** 37 * Information we keep per coin to validate the reply. 38 */ 39 struct CoinData 40 { 41 /** 42 * Public key of the coin. 43 */ 44 struct TALER_CoinSpendPublicKeyP coin_pub; 45 46 /** 47 * Signature by the coin. 48 */ 49 struct TALER_CoinSpendSignatureP coin_sig; 50 51 /** 52 * The hash of the denomination's public key 53 */ 54 struct TALER_DenominationHashP h_denom_pub; 55 56 /** 57 * How much did this coin contribute. 58 */ 59 struct TALER_Amount contribution; 60 }; 61 62 63 /** 64 * @brief A POST /reserves/$RID/open Handle 65 */ 66 struct TALER_EXCHANGE_PostReservesOpenHandle 67 { 68 69 /** 70 * Reference to the execution context. 71 */ 72 struct GNUNET_CURL_Context *ctx; 73 74 /** 75 * Base URL of the exchange. 76 */ 77 char *base_url; 78 79 /** 80 * The url for this request, set during _start. 81 */ 82 char *url; 83 84 /** 85 * Context for #TEH_curl_easy_post(). Keeps the data that must 86 * persist for Curl to make the upload. 87 */ 88 struct TALER_CURL_PostContext post_ctx; 89 90 /** 91 * Handle for the request. 92 */ 93 struct GNUNET_CURL_Job *job; 94 95 /** 96 * Function to call with the result. 97 */ 98 TALER_EXCHANGE_PostReservesOpenCallback cb; 99 100 /** 101 * Closure for @a cb. 102 */ 103 TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls; 104 105 /** 106 * The keys of the exchange this request handle will use 107 */ 108 struct TALER_EXCHANGE_Keys *keys; 109 110 /** 111 * Public key of the reserve we are querying. 112 */ 113 struct TALER_ReservePublicKeyP reserve_pub; 114 115 /** 116 * Information we keep per coin to validate the reply. 117 */ 118 struct CoinData *coins; 119 120 /** 121 * Length of the @e coins array. 122 */ 123 unsigned int num_coins; 124 125 /** 126 * Pre-built JSON body for the request. 127 */ 128 json_t *body; 129 130 }; 131 132 133 /** 134 * We received an #MHD_HTTP_OK open code. Handle the JSON 135 * response. 136 * 137 * @param proh handle of the request 138 * @param j JSON response 139 * @return #GNUNET_OK on success 140 */ 141 static enum GNUNET_GenericReturnValue 142 handle_reserves_open_ok (struct TALER_EXCHANGE_PostReservesOpenHandle *proh, 143 const json_t *j) 144 { 145 struct TALER_EXCHANGE_PostReservesOpenResponse rs = { 146 .hr.reply = j, 147 .hr.http_status = MHD_HTTP_OK, 148 }; 149 struct GNUNET_JSON_Specification spec[] = { 150 TALER_JSON_spec_amount_any ("open_cost", 151 &rs.details.ok.open_cost), 152 GNUNET_JSON_spec_timestamp ("reserve_expiration", 153 &rs.details.ok.expiration_time), 154 GNUNET_JSON_spec_end () 155 }; 156 157 if (GNUNET_OK != 158 GNUNET_JSON_parse (j, 159 spec, 160 NULL, 161 NULL)) 162 { 163 GNUNET_break_op (0); 164 return GNUNET_SYSERR; 165 } 166 proh->cb (proh->cb_cls, 167 &rs); 168 proh->cb = NULL; 169 GNUNET_JSON_parse_free (spec); 170 return GNUNET_OK; 171 } 172 173 174 /** 175 * We received an #MHD_HTTP_PAYMENT_REQUIRED open code. Handle the JSON 176 * response. 177 * 178 * @param proh handle of the request 179 * @param j JSON response 180 * @return #GNUNET_OK on success 181 */ 182 static enum GNUNET_GenericReturnValue 183 handle_reserves_open_pr (struct TALER_EXCHANGE_PostReservesOpenHandle *proh, 184 const json_t *j) 185 { 186 struct TALER_EXCHANGE_PostReservesOpenResponse rs = { 187 .hr.reply = j, 188 .hr.http_status = MHD_HTTP_PAYMENT_REQUIRED, 189 }; 190 struct GNUNET_JSON_Specification spec[] = { 191 TALER_JSON_spec_amount_any ("open_cost", 192 &rs.details.payment_required.open_cost), 193 GNUNET_JSON_spec_timestamp ("reserve_expiration", 194 &rs.details.payment_required.expiration_time), 195 GNUNET_JSON_spec_end () 196 }; 197 198 if (GNUNET_OK != 199 GNUNET_JSON_parse (j, 200 spec, 201 NULL, 202 NULL)) 203 { 204 GNUNET_break_op (0); 205 return GNUNET_SYSERR; 206 } 207 proh->cb (proh->cb_cls, 208 &rs); 209 proh->cb = NULL; 210 GNUNET_JSON_parse_free (spec); 211 return GNUNET_OK; 212 } 213 214 215 /** 216 * We received an #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS open code. Handle the JSON 217 * response. 218 * 219 * @param proh handle of the request 220 * @param j JSON response 221 * @return #GNUNET_OK on success 222 */ 223 static enum GNUNET_GenericReturnValue 224 handle_reserves_open_kyc (struct TALER_EXCHANGE_PostReservesOpenHandle *proh, 225 const json_t *j) 226 { 227 struct TALER_EXCHANGE_PostReservesOpenResponse rs = { 228 .hr.reply = j, 229 .hr.http_status = MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, 230 }; 231 struct GNUNET_JSON_Specification spec[] = { 232 GNUNET_JSON_spec_fixed_auto ( 233 "h_payto", 234 &rs.details.unavailable_for_legal_reasons.h_payto), 235 GNUNET_JSON_spec_uint64 ( 236 "requirement_row", 237 &rs.details.unavailable_for_legal_reasons.requirement_row), 238 GNUNET_JSON_spec_end () 239 }; 240 241 if (GNUNET_OK != 242 GNUNET_JSON_parse (j, 243 spec, 244 NULL, 245 NULL)) 246 { 247 GNUNET_break_op (0); 248 return GNUNET_SYSERR; 249 } 250 proh->cb (proh->cb_cls, 251 &rs); 252 proh->cb = NULL; 253 GNUNET_JSON_parse_free (spec); 254 return GNUNET_OK; 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 break; 368 } 369 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 370 if (GNUNET_OK != 371 handle_reserves_open_kyc (proh, 372 j)) 373 { 374 GNUNET_break_op (0); 375 rs.hr.http_status = 0; 376 rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 377 } 378 break; 379 case MHD_HTTP_INTERNAL_SERVER_ERROR: 380 /* Server had an internal issue; we should retry, but this API 381 leaves this to the application */ 382 rs.hr.ec = TALER_JSON_get_error_code (j); 383 rs.hr.hint = TALER_JSON_get_error_hint (j); 384 break; 385 default: 386 /* unexpected response code */ 387 GNUNET_break_op (0); 388 rs.hr.ec = TALER_JSON_get_error_code (j); 389 rs.hr.hint = TALER_JSON_get_error_hint (j); 390 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 391 "Unexpected response code %u/%d for reserves open\n", 392 (unsigned int) response_code, 393 (int) rs.hr.ec); 394 break; 395 } 396 if (NULL != proh->cb) 397 { 398 proh->cb (proh->cb_cls, 399 &rs); 400 proh->cb = NULL; 401 } 402 TALER_EXCHANGE_post_reserves_open_cancel (proh); 403 } 404 405 406 struct TALER_EXCHANGE_PostReservesOpenHandle * 407 TALER_EXCHANGE_post_reserves_open_create ( 408 struct GNUNET_CURL_Context *ctx, 409 const char *url, 410 struct TALER_EXCHANGE_Keys *keys, 411 const struct TALER_ReservePrivateKeyP *reserve_priv, 412 const struct TALER_Amount *reserve_contribution, 413 unsigned int coin_payments_length, 414 const struct TALER_EXCHANGE_PurseDeposit coin_payments[ 415 static coin_payments_length], 416 struct GNUNET_TIME_Timestamp expiration_time, 417 uint32_t min_purses) 418 { 419 struct TALER_EXCHANGE_PostReservesOpenHandle *proh; 420 struct GNUNET_TIME_Timestamp ts; 421 struct TALER_ReserveSignatureP reserve_sig; 422 json_t *cpa; 423 424 proh = GNUNET_new (struct TALER_EXCHANGE_PostReservesOpenHandle); 425 proh->ctx = ctx; 426 proh->base_url = GNUNET_strdup (url); 427 proh->keys = TALER_EXCHANGE_keys_incref (keys); 428 ts = GNUNET_TIME_timestamp_get (); 429 GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, 430 &proh->reserve_pub.eddsa_pub); 431 TALER_wallet_reserve_open_sign (reserve_contribution, 432 ts, 433 expiration_time, 434 min_purses, 435 reserve_priv, 436 &reserve_sig); 437 proh->coins = GNUNET_new_array (coin_payments_length, 438 struct CoinData); 439 proh->num_coins = coin_payments_length; 440 cpa = json_array (); 441 GNUNET_assert (NULL != cpa); 442 for (unsigned int i = 0; i < coin_payments_length; i++) 443 { 444 const struct TALER_EXCHANGE_PurseDeposit *pd = &coin_payments[i]; 445 const struct TALER_AgeCommitmentProof *acp = pd->age_commitment_proof; 446 struct TALER_AgeCommitmentHashP ahac; 447 struct TALER_AgeCommitmentHashP *achp = NULL; 448 struct CoinData *cd = &proh->coins[i]; 449 json_t *cp; 450 451 cd->contribution = pd->amount; 452 cd->h_denom_pub = pd->h_denom_pub; 453 if (NULL != acp) 454 { 455 TALER_age_commitment_hash (&acp->commitment, 456 &ahac); 457 achp = &ahac; 458 } 459 TALER_wallet_reserve_open_deposit_sign (&pd->amount, 460 &reserve_sig, 461 &pd->coin_priv, 462 &cd->coin_sig); 463 GNUNET_CRYPTO_eddsa_key_get_public (&pd->coin_priv.eddsa_priv, 464 &cd->coin_pub.eddsa_pub); 465 466 cp = GNUNET_JSON_PACK ( 467 GNUNET_JSON_pack_allow_null ( 468 GNUNET_JSON_pack_data_auto ("h_age_commitment", 469 achp)), 470 TALER_JSON_pack_amount ("amount", 471 &pd->amount), 472 GNUNET_JSON_pack_data_auto ("denom_pub_hash", 473 &pd->h_denom_pub), 474 TALER_JSON_pack_denom_sig ("ub_sig", 475 &pd->denom_sig), 476 GNUNET_JSON_pack_data_auto ("coin_pub", 477 &cd->coin_pub), 478 GNUNET_JSON_pack_data_auto ("coin_sig", 479 &cd->coin_sig)); 480 GNUNET_assert (0 == 481 json_array_append_new (cpa, 482 cp)); 483 } 484 proh->body = GNUNET_JSON_PACK ( 485 GNUNET_JSON_pack_timestamp ("request_timestamp", 486 ts), 487 GNUNET_JSON_pack_timestamp ("reserve_expiration", 488 expiration_time), 489 GNUNET_JSON_pack_array_steal ("payments", 490 cpa), 491 TALER_JSON_pack_amount ("reserve_payment", 492 reserve_contribution), 493 GNUNET_JSON_pack_uint64 ("purse_limit", 494 min_purses), 495 GNUNET_JSON_pack_data_auto ("reserve_sig", 496 &reserve_sig)); 497 if (NULL == proh->body) 498 { 499 GNUNET_break (0); 500 GNUNET_free (proh->coins); 501 GNUNET_free (proh->base_url); 502 TALER_EXCHANGE_keys_decref (proh->keys); 503 GNUNET_free (proh); 504 return NULL; 505 } 506 return proh; 507 } 508 509 510 enum TALER_ErrorCode 511 TALER_EXCHANGE_post_reserves_open_start ( 512 struct TALER_EXCHANGE_PostReservesOpenHandle *proh, 513 TALER_EXCHANGE_PostReservesOpenCallback cb, 514 TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls) 515 { 516 CURL *eh; 517 char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; 518 519 proh->cb = cb; 520 proh->cb_cls = cb_cls; 521 { 522 char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; 523 char *end; 524 525 end = GNUNET_STRINGS_data_to_string ( 526 &proh->reserve_pub, 527 sizeof (proh->reserve_pub), 528 pub_str, 529 sizeof (pub_str)); 530 *end = '\0'; 531 GNUNET_snprintf (arg_str, 532 sizeof (arg_str), 533 "reserves/%s/open", 534 pub_str); 535 } 536 proh->url = TALER_url_join (proh->base_url, 537 arg_str, 538 NULL); 539 if (NULL == proh->url) 540 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 541 eh = TALER_EXCHANGE_curl_easy_get_ (proh->url); 542 if ( (NULL == eh) || 543 (GNUNET_OK != 544 TALER_curl_easy_post (&proh->post_ctx, 545 eh, 546 proh->body)) ) 547 { 548 GNUNET_break (0); 549 if (NULL != eh) 550 curl_easy_cleanup (eh); 551 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 552 } 553 proh->job = GNUNET_CURL_job_add2 (proh->ctx, 554 eh, 555 proh->post_ctx.headers, 556 &handle_reserves_open_finished, 557 proh); 558 if (NULL == proh->job) 559 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 560 return TALER_EC_NONE; 561 } 562 563 564 void 565 TALER_EXCHANGE_post_reserves_open_cancel ( 566 struct TALER_EXCHANGE_PostReservesOpenHandle *proh) 567 { 568 if (NULL != proh->job) 569 { 570 GNUNET_CURL_job_cancel (proh->job); 571 proh->job = NULL; 572 } 573 TALER_curl_easy_post_finished (&proh->post_ctx); 574 json_decref (proh->body); 575 GNUNET_free (proh->coins); 576 GNUNET_free (proh->url); 577 GNUNET_free (proh->base_url); 578 TALER_EXCHANGE_keys_decref (proh->keys); 579 GNUNET_free (proh); 580 } 581 582 583 /* end of exchange_api_post-reserves-RESERVE_PUB-open.c */