exchange_api_post-reserves-RESERVE_PUB-purse.c (20652B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022-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-purse.c 19 * @brief Implementation of the client to create a 20 * purse for an account 21 * @author Christian Grothoff 22 */ 23 #include "taler/platform.h" 24 #include <jansson.h> 25 #include <microhttpd.h> /* just for HTTP status codes */ 26 #include <gnunet/gnunet_util_lib.h> 27 #include <gnunet/gnunet_json_lib.h> 28 #include <gnunet/gnunet_curl_lib.h> 29 #include "taler/taler_json_lib.h" 30 #include "taler/taler_exchange_service.h" 31 #include "exchange_api_handle.h" 32 #include "exchange_api_common.h" 33 #include "taler/taler_signatures.h" 34 #include "exchange_api_curl_defaults.h" 35 36 37 /** 38 * @brief A POST /reserves/$RESERVE_PUB/purse handle 39 */ 40 struct TALER_EXCHANGE_PostReservesPurseHandle 41 { 42 43 /** 44 * The keys of the exchange this request handle will use 45 */ 46 struct TALER_EXCHANGE_Keys *keys; 47 48 /** 49 * Context for #TEH_curl_easy_post(). Keeps the data that must 50 * persist for Curl to make the upload. 51 */ 52 struct TALER_CURL_PostContext ctx; 53 54 /** 55 * The base URL for this request. 56 */ 57 char *base_url; 58 59 /** 60 * The full URL for this request, set during _start. 61 */ 62 char *url; 63 64 /** 65 * The exchange base URL (same as base_url, kept for conflict checks). 66 */ 67 char *exchange_url; 68 69 /** 70 * Reference to the execution context. 71 */ 72 struct GNUNET_CURL_Context *curl_ctx; 73 74 /** 75 * Handle for the request. 76 */ 77 struct GNUNET_CURL_Job *job; 78 79 /** 80 * Function to call with the result. 81 */ 82 TALER_EXCHANGE_PostReservesPurseCallback cb; 83 84 /** 85 * Closure for @a cb. 86 */ 87 TALER_EXCHANGE_POST_RESERVES_PURSE_RESULT_CLOSURE *cb_cls; 88 89 /** 90 * Private key for the contract. 91 */ 92 struct TALER_ContractDiffiePrivateP contract_priv; 93 94 /** 95 * Private key for the purse. 96 */ 97 struct TALER_PurseContractPrivateKeyP purse_priv; 98 99 /** 100 * Private key of the reserve. 101 */ 102 struct TALER_ReservePrivateKeyP reserve_priv; 103 104 /** 105 * The encrypted contract (if any). 106 */ 107 struct TALER_EncryptedContract econtract; 108 109 /** 110 * Expected value in the purse after fees. 111 */ 112 struct TALER_Amount purse_value_after_fees; 113 114 /** 115 * Public key of the reserve public key. 116 */ 117 struct TALER_ReservePublicKeyP reserve_pub; 118 119 /** 120 * Reserve signature affirming our merge. 121 */ 122 struct TALER_ReserveSignatureP reserve_sig; 123 124 /** 125 * Merge capability key. 126 */ 127 struct TALER_PurseMergePublicKeyP merge_pub; 128 129 /** 130 * Our merge signature (if any). 131 */ 132 struct TALER_PurseMergeSignatureP merge_sig; 133 134 /** 135 * Public key of the purse. 136 */ 137 struct TALER_PurseContractPublicKeyP purse_pub; 138 139 /** 140 * Request data we signed over. 141 */ 142 struct TALER_PurseContractSignatureP purse_sig; 143 144 /** 145 * Hash over the purse's contract terms. 146 */ 147 struct TALER_PrivateContractHashP h_contract_terms; 148 149 /** 150 * When does the purse expire. 151 */ 152 struct GNUNET_TIME_Timestamp purse_expiration; 153 154 /** 155 * When does the purse get merged/created. 156 */ 157 struct GNUNET_TIME_Timestamp merge_timestamp; 158 159 /** 160 * Our contract terms. 161 */ 162 json_t *contract_terms; 163 164 /** 165 * Minimum age for the coins as per @e contract_terms. 166 */ 167 uint32_t min_age; 168 169 struct 170 { 171 172 /** 173 * Are we paying for purse creation? Not yet a "real" option. 174 */ 175 bool pay_for_purse; 176 177 /** 178 * Should we upload the contract? 179 */ 180 bool upload_contract; 181 } options; 182 183 }; 184 185 186 /** 187 * Function called when we're done processing the 188 * HTTP /reserves/$RID/purse request. 189 * 190 * @param cls the `struct TALER_EXCHANGE_PostReservesPurseHandle` 191 * @param response_code HTTP response code, 0 on error 192 * @param response parsed JSON result, NULL on error 193 */ 194 static void 195 handle_purse_create_with_merge_finished (void *cls, 196 long response_code, 197 const void *response) 198 { 199 struct TALER_EXCHANGE_PostReservesPurseHandle *prph = cls; 200 const json_t *j = response; 201 struct TALER_EXCHANGE_PostReservesPurseResponse dr = { 202 .hr.reply = j, 203 .hr.http_status = (unsigned int) response_code, 204 .reserve_sig = &prph->reserve_sig 205 }; 206 207 prph->job = NULL; 208 switch (response_code) 209 { 210 case 0: 211 dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 212 break; 213 case MHD_HTTP_OK: 214 { 215 struct GNUNET_TIME_Timestamp etime; 216 struct TALER_Amount total_deposited; 217 struct TALER_ExchangeSignatureP exchange_sig; 218 struct TALER_ExchangePublicKeyP exchange_pub; 219 struct GNUNET_JSON_Specification spec[] = { 220 TALER_JSON_spec_amount_any ("total_deposited", 221 &total_deposited), 222 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 223 &exchange_sig), 224 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 225 &exchange_pub), 226 GNUNET_JSON_spec_timestamp ("exchange_timestamp", 227 &etime), 228 GNUNET_JSON_spec_end () 229 }; 230 231 if (GNUNET_OK != 232 GNUNET_JSON_parse (j, 233 spec, 234 NULL, NULL)) 235 { 236 GNUNET_break_op (0); 237 dr.hr.http_status = 0; 238 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 239 break; 240 } 241 if (GNUNET_OK != 242 TALER_EXCHANGE_test_signing_key (prph->keys, 243 &exchange_pub)) 244 { 245 GNUNET_break_op (0); 246 dr.hr.http_status = 0; 247 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 248 break; 249 } 250 if (GNUNET_OK != 251 TALER_exchange_online_purse_created_verify ( 252 etime, 253 prph->purse_expiration, 254 &prph->purse_value_after_fees, 255 &total_deposited, 256 &prph->purse_pub, 257 &prph->h_contract_terms, 258 &exchange_pub, 259 &exchange_sig)) 260 { 261 GNUNET_break_op (0); 262 dr.hr.http_status = 0; 263 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 264 break; 265 } 266 } 267 break; 268 case MHD_HTTP_BAD_REQUEST: 269 /* This should never happen, either us or the exchange is buggy 270 (or API version conflict); just pass JSON reply to the application */ 271 dr.hr.ec = TALER_JSON_get_error_code (j); 272 dr.hr.hint = TALER_JSON_get_error_hint (j); 273 break; 274 case MHD_HTTP_FORBIDDEN: 275 dr.hr.ec = TALER_JSON_get_error_code (j); 276 dr.hr.hint = TALER_JSON_get_error_hint (j); 277 /* Nothing really to verify, exchange says one of the signatures is 278 invalid; as we checked them, this should never happen, we 279 should pass the JSON reply to the application */ 280 break; 281 case MHD_HTTP_NOT_FOUND: 282 dr.hr.ec = TALER_JSON_get_error_code (j); 283 dr.hr.hint = TALER_JSON_get_error_hint (j); 284 /* Nothing really to verify, this should never 285 happen, we should pass the JSON reply to the application */ 286 break; 287 case MHD_HTTP_CONFLICT: 288 dr.hr.ec = TALER_JSON_get_error_code (j); 289 switch (dr.hr.ec) 290 { 291 case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA: 292 if (GNUNET_OK != 293 TALER_EXCHANGE_check_purse_create_conflict_ ( 294 &prph->purse_sig, 295 &prph->purse_pub, 296 j)) 297 { 298 dr.hr.http_status = 0; 299 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 300 break; 301 } 302 break; 303 case TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA: 304 if (GNUNET_OK != 305 TALER_EXCHANGE_check_purse_merge_conflict_ ( 306 &prph->merge_sig, 307 &prph->merge_pub, 308 &prph->purse_pub, 309 prph->exchange_url, 310 j)) 311 { 312 GNUNET_break_op (0); 313 dr.hr.http_status = 0; 314 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 315 break; 316 } 317 break; 318 case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS: 319 /* nothing to verify */ 320 break; 321 case TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA: 322 if (GNUNET_OK != 323 TALER_EXCHANGE_check_purse_econtract_conflict_ ( 324 &prph->econtract.econtract_sig, 325 &prph->purse_pub, 326 j)) 327 { 328 GNUNET_break_op (0); 329 dr.hr.http_status = 0; 330 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 331 break; 332 } 333 break; 334 default: 335 /* unexpected EC! */ 336 GNUNET_break_op (0); 337 dr.hr.http_status = 0; 338 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 339 break; 340 } /* end inner (EC) switch */ 341 break; 342 case MHD_HTTP_GONE: 343 /* could happen if denomination was revoked */ 344 /* Note: one might want to check /keys for revocation 345 signature here, alas tricky in case our /keys 346 is outdated => left to clients */ 347 dr.hr.ec = TALER_JSON_get_error_code (j); 348 dr.hr.hint = TALER_JSON_get_error_hint (j); 349 break; 350 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 351 { 352 struct GNUNET_JSON_Specification spec[] = { 353 GNUNET_JSON_spec_uint64 ( 354 "requirement_row", 355 &dr.details.unavailable_for_legal_reasons.requirement_row), 356 GNUNET_JSON_spec_end () 357 }; 358 359 if (GNUNET_OK != 360 GNUNET_JSON_parse (j, 361 spec, 362 NULL, NULL)) 363 { 364 GNUNET_break_op (0); 365 dr.hr.http_status = 0; 366 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 367 break; 368 } 369 } 370 break; 371 case MHD_HTTP_INTERNAL_SERVER_ERROR: 372 dr.hr.ec = TALER_JSON_get_error_code (j); 373 dr.hr.hint = TALER_JSON_get_error_hint (j); 374 /* Server had an internal issue; we should retry, but this API 375 leaves this to the application */ 376 break; 377 default: 378 /* unexpected response code */ 379 dr.hr.ec = TALER_JSON_get_error_code (j); 380 dr.hr.hint = TALER_JSON_get_error_hint (j); 381 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 382 "Unexpected response code %u/%d for exchange deposit\n", 383 (unsigned int) response_code, 384 dr.hr.ec); 385 GNUNET_break_op (0); 386 break; 387 } 388 if (NULL != prph->cb) 389 { 390 prph->cb (prph->cb_cls, 391 &dr); 392 prph->cb = NULL; 393 } 394 TALER_EXCHANGE_post_reserves_purse_cancel (prph); 395 } 396 397 398 struct TALER_EXCHANGE_PostReservesPurseHandle * 399 TALER_EXCHANGE_post_reserves_purse_create ( 400 struct GNUNET_CURL_Context *ctx, 401 const char *url, 402 struct TALER_EXCHANGE_Keys *keys, 403 const struct TALER_ReservePrivateKeyP *reserve_priv, 404 const struct TALER_PurseContractPrivateKeyP *purse_priv, 405 const struct TALER_PurseMergePrivateKeyP *merge_priv, 406 const struct TALER_ContractDiffiePrivateP *contract_priv, 407 const json_t *contract_terms, 408 bool pay_for_purse, // FIXME: turn into option? 409 struct GNUNET_TIME_Timestamp merge_timestamp) // FIXME: turn into option? 410 { 411 struct TALER_EXCHANGE_PostReservesPurseHandle *prph; 412 413 prph = GNUNET_new (struct TALER_EXCHANGE_PostReservesPurseHandle); 414 prph->curl_ctx = ctx; 415 prph->keys = TALER_EXCHANGE_keys_incref (keys); 416 prph->base_url = GNUNET_strdup (url); 417 prph->contract_terms = json_incref ((json_t *) contract_terms); 418 prph->exchange_url = GNUNET_strdup (url); 419 prph->contract_priv = *contract_priv; 420 prph->reserve_priv = *reserve_priv; 421 prph->purse_priv = *purse_priv; 422 prph->options.pay_for_purse = pay_for_purse; 423 424 if (GNUNET_OK != 425 TALER_JSON_contract_hash (contract_terms, 426 &prph->h_contract_terms)) 427 { 428 GNUNET_break (0); 429 TALER_EXCHANGE_keys_decref (prph->keys); 430 GNUNET_free (prph->base_url); 431 GNUNET_free (prph->exchange_url); 432 GNUNET_free (prph); 433 return NULL; 434 } 435 prph->merge_timestamp = merge_timestamp; 436 GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv, 437 &prph->purse_pub.eddsa_pub); 438 GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, 439 &prph->reserve_pub.eddsa_pub); 440 GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv, 441 &prph->merge_pub.eddsa_pub); 442 443 { 444 struct GNUNET_JSON_Specification spec[] = { 445 TALER_JSON_spec_amount_any ("amount", 446 &prph->purse_value_after_fees), 447 GNUNET_JSON_spec_mark_optional ( 448 GNUNET_JSON_spec_uint32 ("minimum_age", 449 &prph->min_age), 450 NULL), 451 GNUNET_JSON_spec_timestamp ("pay_deadline", 452 &prph->purse_expiration), 453 GNUNET_JSON_spec_end () 454 }; 455 456 if (GNUNET_OK != 457 GNUNET_JSON_parse (contract_terms, 458 spec, 459 NULL, NULL)) 460 { 461 GNUNET_break (0); 462 TALER_EXCHANGE_keys_decref (prph->keys); 463 GNUNET_free (prph->base_url); 464 GNUNET_free (prph->exchange_url); 465 GNUNET_free (prph); 466 return NULL; 467 } 468 } 469 470 TALER_wallet_purse_create_sign (prph->purse_expiration, 471 &prph->h_contract_terms, 472 &prph->merge_pub, 473 prph->min_age, 474 &prph->purse_value_after_fees, 475 purse_priv, 476 &prph->purse_sig); 477 { 478 struct TALER_NormalizedPayto payto_uri; 479 480 payto_uri = TALER_reserve_make_payto (url, 481 &prph->reserve_pub); 482 TALER_wallet_purse_merge_sign (payto_uri, 483 prph->merge_timestamp, 484 &prph->purse_pub, 485 merge_priv, 486 &prph->merge_sig); 487 GNUNET_free (payto_uri.normalized_payto); 488 } 489 return prph; 490 } 491 492 493 enum GNUNET_GenericReturnValue 494 TALER_EXCHANGE_post_reserves_purse_set_options_ ( 495 struct TALER_EXCHANGE_PostReservesPurseHandle *prph, 496 unsigned int num_options, 497 const struct TALER_EXCHANGE_PostReservesPurseOptionValue options[]) 498 { 499 for (unsigned int i = 0; i < num_options; i++) 500 { 501 const struct TALER_EXCHANGE_PostReservesPurseOptionValue *opt = &options[i]; 502 503 switch (opt->option) 504 { 505 case TALER_EXCHANGE_POST_RESERVES_PURSE_OPTION_END: 506 return GNUNET_OK; 507 case TALER_EXCHANGE_POST_RESERVES_PURSE_OPTION_UPLOAD_CONTRACT: 508 prph->options.upload_contract = true; 509 break; 510 } 511 } 512 return GNUNET_OK; 513 } 514 515 516 enum TALER_ErrorCode 517 TALER_EXCHANGE_post_reserves_purse_start ( 518 struct TALER_EXCHANGE_PostReservesPurseHandle *prph, 519 TALER_EXCHANGE_PostReservesPurseCallback cb, 520 TALER_EXCHANGE_POST_RESERVES_PURSE_RESULT_CLOSURE *cb_cls) 521 { 522 char arg_str[sizeof (prph->reserve_pub) * 2 + 32]; 523 CURL *eh; 524 json_t *body; 525 struct TALER_Amount purse_fee; 526 enum TALER_WalletAccountMergeFlags flags; 527 528 prph->cb = cb; 529 prph->cb_cls = cb_cls; 530 if (prph->options.pay_for_purse) 531 { 532 const struct TALER_EXCHANGE_GlobalFee *gf; 533 534 flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; 535 gf = TALER_EXCHANGE_get_global_fee ( 536 prph->keys, 537 GNUNET_TIME_timestamp_get ()); 538 purse_fee = gf->fees.purse; 539 } 540 else 541 { 542 flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; 543 GNUNET_assert (GNUNET_OK == 544 TALER_amount_set_zero (prph->purse_value_after_fees.currency, 545 &purse_fee)); 546 } 547 548 TALER_wallet_account_merge_sign (prph->merge_timestamp, 549 &prph->purse_pub, 550 prph->purse_expiration, 551 &prph->h_contract_terms, 552 &prph->purse_value_after_fees, 553 &purse_fee, 554 prph->min_age, 555 flags, 556 &prph->reserve_priv, 557 &prph->reserve_sig); 558 559 560 if (prph->options.upload_contract) 561 { 562 TALER_CRYPTO_contract_encrypt_for_deposit ( 563 &prph->purse_pub, 564 &prph->contract_priv, 565 prph->contract_terms, 566 &prph->econtract.econtract, 567 &prph->econtract.econtract_size); 568 GNUNET_CRYPTO_ecdhe_key_get_public ( 569 &prph->contract_priv.ecdhe_priv, 570 &prph->econtract.contract_pub.ecdhe_pub); 571 TALER_wallet_econtract_upload_sign ( 572 prph->econtract.econtract, 573 prph->econtract.econtract_size, 574 &prph->econtract.contract_pub, 575 &prph->purse_priv, 576 &prph->econtract.econtract_sig); 577 } 578 579 body = GNUNET_JSON_PACK ( 580 TALER_JSON_pack_amount ("purse_value", 581 &prph->purse_value_after_fees), 582 GNUNET_JSON_pack_uint64 ("min_age", 583 prph->min_age), 584 GNUNET_JSON_pack_allow_null ( 585 TALER_JSON_pack_econtract ("econtract", 586 prph->options.upload_contract 587 ? &prph->econtract 588 : NULL)), 589 GNUNET_JSON_pack_allow_null ( 590 prph->options.pay_for_purse 591 ? TALER_JSON_pack_amount ("purse_fee", 592 &purse_fee) 593 : GNUNET_JSON_pack_string ("dummy2", 594 NULL)), 595 GNUNET_JSON_pack_data_auto ("merge_pub", 596 &prph->merge_pub), 597 GNUNET_JSON_pack_data_auto ("merge_sig", 598 &prph->merge_sig), 599 GNUNET_JSON_pack_data_auto ("reserve_sig", 600 &prph->reserve_sig), 601 GNUNET_JSON_pack_data_auto ("purse_pub", 602 &prph->purse_pub), 603 GNUNET_JSON_pack_data_auto ("purse_sig", 604 &prph->purse_sig), 605 GNUNET_JSON_pack_data_auto ("h_contract_terms", 606 &prph->h_contract_terms), 607 GNUNET_JSON_pack_timestamp ("merge_timestamp", 608 prph->merge_timestamp), 609 GNUNET_JSON_pack_timestamp ("purse_expiration", 610 prph->purse_expiration)); 611 if (NULL == body) 612 return TALER_EC_GENERIC_ALLOCATION_FAILURE; 613 614 { 615 char pub_str[sizeof (prph->reserve_pub) * 2]; 616 char *end; 617 618 end = GNUNET_STRINGS_data_to_string ( 619 &prph->reserve_pub, 620 sizeof (prph->reserve_pub), 621 pub_str, 622 sizeof (pub_str)); 623 *end = '\0'; 624 GNUNET_snprintf (arg_str, 625 sizeof (arg_str), 626 "reserves/%s/purse", 627 pub_str); 628 } 629 prph->url = TALER_url_join (prph->base_url, 630 arg_str, 631 NULL); 632 if (NULL == prph->url) 633 { 634 GNUNET_break (0); 635 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 636 } 637 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 638 "URL for purse create_with_merge: `%s'\n", 639 prph->url); 640 eh = TALER_EXCHANGE_curl_easy_get_ (prph->url); 641 if ( (NULL == eh) || 642 (GNUNET_OK != 643 TALER_curl_easy_post (&prph->ctx, 644 eh, 645 body)) ) 646 { 647 GNUNET_break (0); 648 if (NULL != eh) 649 curl_easy_cleanup (eh); 650 return TALER_EC_GENERIC_CURL_ALLOCATION_FAILURE; 651 } 652 json_decref (body); 653 prph->job = GNUNET_CURL_job_add2 (prph->curl_ctx, 654 eh, 655 prph->ctx.headers, 656 &handle_purse_create_with_merge_finished, 657 prph); 658 if (NULL == prph->job) 659 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 660 return TALER_EC_NONE; 661 } 662 663 664 void 665 TALER_EXCHANGE_post_reserves_purse_cancel ( 666 struct TALER_EXCHANGE_PostReservesPurseHandle *prph) 667 { 668 if (NULL != prph->job) 669 { 670 GNUNET_CURL_job_cancel (prph->job); 671 prph->job = NULL; 672 } 673 GNUNET_free (prph->url); 674 GNUNET_free (prph->base_url); 675 GNUNET_free (prph->exchange_url); 676 TALER_curl_easy_post_finished (&prph->ctx); 677 TALER_EXCHANGE_keys_decref (prph->keys); 678 GNUNET_free (prph->econtract.econtract); 679 json_decref (prph->contract_terms); 680 GNUNET_free (prph); 681 } 682 683 684 /* end of exchange_api_post-reserves-RESERVE_PUB-purse.c */