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