exchange_api_purse_create_with_deposit.c (20627B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022-2023 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_purse_create_with_deposit.c 19 * @brief Implementation of the client to create a purse with 20 * an initial set of deposits (and a contract) 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 * Information we track per deposited coin. 39 */ 40 struct Deposit 41 { 42 /** 43 * Coin's public key. 44 */ 45 struct TALER_CoinSpendPublicKeyP coin_pub; 46 47 /** 48 * Signature made with the coin. 49 */ 50 struct TALER_CoinSpendSignatureP coin_sig; 51 52 /** 53 * Coin's denomination. 54 */ 55 struct TALER_DenominationHashP h_denom_pub; 56 57 /** 58 * Age restriction hash for the coin. 59 */ 60 struct TALER_AgeCommitmentHashP ahac; 61 62 /** 63 * How much did we say the coin contributed. 64 */ 65 struct TALER_Amount contribution; 66 }; 67 68 69 /** 70 * @brief A purse create with deposit handle 71 */ 72 struct TALER_EXCHANGE_PurseCreateDepositHandle 73 { 74 75 /** 76 * The keys of the exchange this request handle will use 77 */ 78 struct TALER_EXCHANGE_Keys *keys; 79 80 /** 81 * The url for this request. 82 */ 83 char *url; 84 85 /** 86 * The base URL of the exchange. 87 */ 88 char *exchange_url; 89 90 /** 91 * Context for #TEH_curl_easy_post(). Keeps the data that must 92 * persist for Curl to make the upload. 93 */ 94 struct TALER_CURL_PostContext ctx; 95 96 /** 97 * Handle for the request. 98 */ 99 struct GNUNET_CURL_Job *job; 100 101 /** 102 * Function to call with the result. 103 */ 104 TALER_EXCHANGE_PurseCreateDepositCallback cb; 105 106 /** 107 * Closure for @a cb. 108 */ 109 void *cb_cls; 110 111 /** 112 * Expected value in the purse after fees. 113 */ 114 struct TALER_Amount purse_value_after_fees; 115 116 /** 117 * Our encrypted contract (if we had any). 118 */ 119 struct TALER_EncryptedContract econtract; 120 121 /** 122 * Public key of the merge capability. 123 */ 124 struct TALER_PurseMergePublicKeyP merge_pub; 125 126 /** 127 * Public key of the purse. 128 */ 129 struct TALER_PurseContractPublicKeyP purse_pub; 130 131 /** 132 * Signature with the purse key on the request. 133 */ 134 struct TALER_PurseContractSignatureP purse_sig; 135 136 /** 137 * Hash over the purse's contract terms. 138 */ 139 struct TALER_PrivateContractHashP h_contract_terms; 140 141 /** 142 * When does the purse expire. 143 */ 144 struct GNUNET_TIME_Timestamp purse_expiration; 145 146 /** 147 * Array of @e num_deposit deposits. 148 */ 149 struct Deposit *deposits; 150 151 /** 152 * How many deposits did we make? 153 */ 154 unsigned int num_deposits; 155 156 }; 157 158 159 /** 160 * Function called when we're done processing the 161 * HTTP /deposit request. 162 * 163 * @param cls the `struct TALER_EXCHANGE_DepositHandle` 164 * @param response_code HTTP response code, 0 on error 165 * @param response parsed JSON result, NULL on error 166 */ 167 static void 168 handle_purse_create_deposit_finished (void *cls, 169 long response_code, 170 const void *response) 171 { 172 struct TALER_EXCHANGE_PurseCreateDepositHandle *pch = cls; 173 const json_t *j = response; 174 struct TALER_EXCHANGE_PurseCreateDepositResponse dr = { 175 .hr.reply = j, 176 .hr.http_status = (unsigned int) response_code 177 }; 178 const struct TALER_EXCHANGE_Keys *keys = pch->keys; 179 180 pch->job = NULL; 181 switch (response_code) 182 { 183 case 0: 184 dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 185 break; 186 case MHD_HTTP_OK: 187 { 188 struct GNUNET_TIME_Timestamp etime; 189 struct TALER_Amount total_deposited; 190 struct TALER_ExchangeSignatureP exchange_sig; 191 struct TALER_ExchangePublicKeyP exchange_pub; 192 struct GNUNET_JSON_Specification spec[] = { 193 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 194 &exchange_sig), 195 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 196 &exchange_pub), 197 GNUNET_JSON_spec_timestamp ("exchange_timestamp", 198 &etime), 199 TALER_JSON_spec_amount ("total_deposited", 200 pch->purse_value_after_fees.currency, 201 &total_deposited), 202 GNUNET_JSON_spec_end () 203 }; 204 205 if (GNUNET_OK != 206 GNUNET_JSON_parse (j, 207 spec, 208 NULL, NULL)) 209 { 210 GNUNET_break_op (0); 211 dr.hr.http_status = 0; 212 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 213 break; 214 } 215 if (GNUNET_OK != 216 TALER_EXCHANGE_test_signing_key (keys, 217 &exchange_pub)) 218 { 219 GNUNET_break_op (0); 220 dr.hr.http_status = 0; 221 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID; 222 break; 223 } 224 if (GNUNET_OK != 225 TALER_exchange_online_purse_created_verify ( 226 etime, 227 pch->purse_expiration, 228 &pch->purse_value_after_fees, 229 &total_deposited, 230 &pch->purse_pub, 231 &pch->h_contract_terms, 232 &exchange_pub, 233 &exchange_sig)) 234 { 235 GNUNET_break_op (0); 236 dr.hr.http_status = 0; 237 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID; 238 break; 239 } 240 } 241 break; 242 case MHD_HTTP_BAD_REQUEST: 243 /* This should never happen, either us or the exchange is buggy 244 (or API version conflict); just pass JSON reply to the application */ 245 dr.hr.ec = TALER_JSON_get_error_code (j); 246 dr.hr.hint = TALER_JSON_get_error_hint (j); 247 break; 248 case MHD_HTTP_FORBIDDEN: 249 dr.hr.ec = TALER_JSON_get_error_code (j); 250 dr.hr.hint = TALER_JSON_get_error_hint (j); 251 /* Nothing really to verify, exchange says one of the signatures is 252 invalid; as we checked them, this should never happen, we 253 should pass the JSON reply to the application */ 254 break; 255 case MHD_HTTP_NOT_FOUND: 256 dr.hr.ec = TALER_JSON_get_error_code (j); 257 dr.hr.hint = TALER_JSON_get_error_hint (j); 258 /* Nothing really to verify, this should never 259 happen, we should pass the JSON reply to the application */ 260 break; 261 case MHD_HTTP_CONFLICT: 262 { 263 dr.hr.ec = TALER_JSON_get_error_code (j); 264 switch (dr.hr.ec) 265 { 266 case TALER_EC_EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA: 267 if (GNUNET_OK != 268 TALER_EXCHANGE_check_purse_create_conflict_ ( 269 &pch->purse_sig, 270 &pch->purse_pub, 271 j)) 272 { 273 GNUNET_break_op (0); 274 dr.hr.http_status = 0; 275 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 276 break; 277 } 278 break; 279 case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: 280 /* Nothing to check anymore here, proof needs to be 281 checked in the GET /coins/$COIN_PUB handler */ 282 break; 283 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: 284 // FIXME #7267: write check (add to exchange_api_common! */ 285 break; 286 case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA: 287 { 288 struct TALER_CoinSpendPublicKeyP coin_pub; 289 struct TALER_CoinSpendSignatureP coin_sig; 290 struct TALER_DenominationHashP h_denom_pub; 291 struct TALER_AgeCommitmentHashP phac; 292 bool found = false; 293 294 if (GNUNET_OK != 295 TALER_EXCHANGE_check_purse_coin_conflict_ ( 296 &pch->purse_pub, 297 pch->exchange_url, 298 j, 299 &h_denom_pub, 300 &phac, 301 &coin_pub, 302 &coin_sig)) 303 { 304 GNUNET_break_op (0); 305 dr.hr.http_status = 0; 306 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 307 break; 308 } 309 for (unsigned int i = 0; i<pch->num_deposits; i++) 310 { 311 struct Deposit *deposit = &pch->deposits[i]; 312 313 if (0 != 314 GNUNET_memcmp (&coin_pub, 315 &deposit->coin_pub)) 316 continue; 317 if (0 != 318 GNUNET_memcmp (&deposit->h_denom_pub, 319 &h_denom_pub)) 320 { 321 found = true; 322 break; 323 } 324 if (0 != 325 GNUNET_memcmp (&deposit->ahac, 326 &phac)) 327 { 328 found = true; 329 break; 330 } 331 if (0 == 332 GNUNET_memcmp (&coin_sig, 333 &deposit->coin_sig)) 334 { 335 GNUNET_break_op (0); 336 continue; 337 } 338 found = true; 339 break; 340 } 341 if (! found) 342 { 343 /* conflict is for a different coin! */ 344 GNUNET_break_op (0); 345 dr.hr.http_status = 0; 346 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 347 break; 348 } 349 } 350 case TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA: 351 if (GNUNET_OK != 352 TALER_EXCHANGE_check_purse_econtract_conflict_ ( 353 &pch->econtract.econtract_sig, 354 &pch->purse_pub, 355 j)) 356 { 357 GNUNET_break_op (0); 358 dr.hr.http_status = 0; 359 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 360 break; 361 } 362 break; 363 default: 364 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 365 "Unexpected error code %d for conflcting deposit\n", 366 dr.hr.ec); 367 GNUNET_break_op (0); 368 dr.hr.http_status = 0; 369 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 370 } 371 } 372 break; 373 case MHD_HTTP_GONE: 374 /* could happen if denomination was revoked */ 375 /* Note: one might want to check /keys for revocation 376 signature here, alas tricky in case our /keys 377 is outdated => left to clients */ 378 dr.hr.ec = TALER_JSON_get_error_code (j); 379 dr.hr.hint = TALER_JSON_get_error_hint (j); 380 break; 381 case MHD_HTTP_INTERNAL_SERVER_ERROR: 382 dr.hr.ec = TALER_JSON_get_error_code (j); 383 dr.hr.hint = TALER_JSON_get_error_hint (j); 384 /* Server had an internal issue; we should retry, but this API 385 leaves this to the application */ 386 break; 387 default: 388 /* unexpected response code */ 389 dr.hr.ec = TALER_JSON_get_error_code (j); 390 dr.hr.hint = TALER_JSON_get_error_hint (j); 391 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 392 "Unexpected response code %u/%d for exchange deposit\n", 393 (unsigned int) response_code, 394 dr.hr.ec); 395 GNUNET_break_op (0); 396 break; 397 } 398 pch->cb (pch->cb_cls, 399 &dr); 400 TALER_EXCHANGE_purse_create_with_deposit_cancel (pch); 401 } 402 403 404 struct TALER_EXCHANGE_PurseCreateDepositHandle * 405 TALER_EXCHANGE_purse_create_with_deposit ( 406 struct GNUNET_CURL_Context *ctx, 407 const char *url, 408 struct TALER_EXCHANGE_Keys *keys, 409 const struct TALER_PurseContractPrivateKeyP *purse_priv, 410 const struct TALER_PurseMergePrivateKeyP *merge_priv, 411 const struct TALER_ContractDiffiePrivateP *contract_priv, 412 const json_t *contract_terms, 413 unsigned int num_deposits, 414 const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits], 415 bool upload_contract, 416 TALER_EXCHANGE_PurseCreateDepositCallback cb, 417 void *cb_cls) 418 { 419 struct TALER_EXCHANGE_PurseCreateDepositHandle *pch; 420 json_t *create_obj; 421 json_t *deposit_arr; 422 CURL *eh; 423 char arg_str[sizeof (pch->purse_pub) * 2 + 32]; 424 uint32_t min_age = 0; 425 426 pch = GNUNET_new (struct TALER_EXCHANGE_PurseCreateDepositHandle); 427 pch->cb = cb; 428 pch->cb_cls = cb_cls; 429 { 430 struct GNUNET_JSON_Specification spec[] = { 431 GNUNET_JSON_spec_timestamp ("pay_deadline", 432 &pch->purse_expiration), 433 TALER_JSON_spec_amount_any ("amount", 434 &pch->purse_value_after_fees), 435 GNUNET_JSON_spec_mark_optional ( 436 GNUNET_JSON_spec_uint32 ("minimum_age", 437 &min_age), 438 NULL), 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 return NULL; 449 } 450 } 451 if (GNUNET_OK != 452 TALER_JSON_contract_hash (contract_terms, 453 &pch->h_contract_terms)) 454 { 455 GNUNET_break (0); 456 return NULL; 457 } 458 GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv, 459 &pch->purse_pub.eddsa_pub); 460 { 461 char pub_str[sizeof (pch->purse_pub) * 2]; 462 char *end; 463 464 end = GNUNET_STRINGS_data_to_string ( 465 &pch->purse_pub, 466 sizeof (pch->purse_pub), 467 pub_str, 468 sizeof (pub_str)); 469 *end = '\0'; 470 GNUNET_snprintf (arg_str, 471 sizeof (arg_str), 472 "purses/%s/create", 473 pub_str); 474 } 475 GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv, 476 &pch->merge_pub.eddsa_pub); 477 pch->url = TALER_url_join (url, 478 arg_str, 479 NULL); 480 if (NULL == pch->url) 481 { 482 GNUNET_break (0); 483 GNUNET_free (pch); 484 return NULL; 485 } 486 pch->num_deposits = num_deposits; 487 pch->deposits = GNUNET_new_array (num_deposits, 488 struct Deposit); 489 deposit_arr = json_array (); 490 GNUNET_assert (NULL != deposit_arr); 491 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 492 "Signing with URL `%s'\n", 493 url); 494 for (unsigned int i = 0; i<num_deposits; i++) 495 { 496 const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i]; 497 const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof; 498 struct Deposit *d = &pch->deposits[i]; 499 json_t *jdeposit; 500 struct TALER_AgeCommitmentHashP *aghp = NULL; 501 struct TALER_AgeAttestationP attest; 502 struct TALER_AgeAttestationP *attestp = NULL; 503 504 if (NULL != acp) 505 { 506 TALER_age_commitment_hash (&acp->commitment, 507 &d->ahac); 508 aghp = &d->ahac; 509 if (GNUNET_OK != 510 TALER_age_commitment_attest (acp, 511 min_age, 512 &attest)) 513 { 514 GNUNET_break (0); 515 GNUNET_array_grow (pch->deposits, 516 pch->num_deposits, 517 0); 518 GNUNET_free (pch->url); 519 json_decref (deposit_arr); 520 GNUNET_free (pch); 521 return NULL; 522 } 523 } 524 d->contribution = deposit->amount; 525 d->h_denom_pub = deposit->h_denom_pub; 526 GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, 527 &d->coin_pub.eddsa_pub); 528 TALER_wallet_purse_deposit_sign ( 529 url, 530 &pch->purse_pub, 531 &deposit->amount, 532 &d->h_denom_pub, 533 &d->ahac, 534 &deposit->coin_priv, 535 &d->coin_sig); 536 jdeposit = GNUNET_JSON_PACK ( 537 GNUNET_JSON_pack_allow_null ( 538 GNUNET_JSON_pack_data_auto ("h_age_commitment", 539 aghp)), 540 GNUNET_JSON_pack_allow_null ( 541 GNUNET_JSON_pack_data_auto ("age_attestation", 542 attestp)), 543 TALER_JSON_pack_amount ("amount", 544 &deposit->amount), 545 GNUNET_JSON_pack_data_auto ("denom_pub_hash", 546 &deposit->h_denom_pub), 547 TALER_JSON_pack_denom_sig ("ub_sig", 548 &deposit->denom_sig), 549 GNUNET_JSON_pack_data_auto ("coin_sig", 550 &d->coin_sig), 551 GNUNET_JSON_pack_data_auto ("coin_pub", 552 &d->coin_pub)); 553 GNUNET_assert (0 == 554 json_array_append_new (deposit_arr, 555 jdeposit)); 556 } 557 TALER_wallet_purse_create_sign (pch->purse_expiration, 558 &pch->h_contract_terms, 559 &pch->merge_pub, 560 min_age, 561 &pch->purse_value_after_fees, 562 purse_priv, 563 &pch->purse_sig); 564 if (upload_contract) 565 { 566 TALER_CRYPTO_contract_encrypt_for_merge (&pch->purse_pub, 567 contract_priv, 568 merge_priv, 569 contract_terms, 570 &pch->econtract.econtract, 571 &pch->econtract.econtract_size); 572 GNUNET_CRYPTO_ecdhe_key_get_public (&contract_priv->ecdhe_priv, 573 &pch->econtract.contract_pub.ecdhe_pub); 574 TALER_wallet_econtract_upload_sign (pch->econtract.econtract, 575 pch->econtract.econtract_size, 576 &pch->econtract.contract_pub, 577 purse_priv, 578 &pch->econtract.econtract_sig); 579 } 580 create_obj = GNUNET_JSON_PACK ( 581 TALER_JSON_pack_amount ("amount", 582 &pch->purse_value_after_fees), 583 GNUNET_JSON_pack_uint64 ("min_age", 584 min_age), 585 GNUNET_JSON_pack_allow_null ( 586 TALER_JSON_pack_econtract ("econtract", 587 upload_contract 588 ? &pch->econtract 589 : NULL)), 590 GNUNET_JSON_pack_data_auto ("purse_sig", 591 &pch->purse_sig), 592 GNUNET_JSON_pack_data_auto ("merge_pub", 593 &pch->merge_pub), 594 GNUNET_JSON_pack_data_auto ("h_contract_terms", 595 &pch->h_contract_terms), 596 GNUNET_JSON_pack_timestamp ("purse_expiration", 597 pch->purse_expiration), 598 GNUNET_JSON_pack_array_steal ("deposits", 599 deposit_arr)); 600 GNUNET_assert (NULL != create_obj); 601 eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); 602 if ( (NULL == eh) || 603 (GNUNET_OK != 604 TALER_curl_easy_post (&pch->ctx, 605 eh, 606 create_obj)) ) 607 { 608 GNUNET_break (0); 609 if (NULL != eh) 610 curl_easy_cleanup (eh); 611 json_decref (create_obj); 612 GNUNET_free (pch->econtract.econtract); 613 GNUNET_array_grow (pch->deposits, 614 pch->num_deposits, 615 0); 616 GNUNET_free (pch->url); 617 GNUNET_free (pch); 618 return NULL; 619 } 620 json_decref (create_obj); 621 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 622 "URL for purse create with deposit: `%s'\n", 623 pch->url); 624 pch->keys = TALER_EXCHANGE_keys_incref (keys); 625 pch->exchange_url = GNUNET_strdup (url); 626 pch->job = GNUNET_CURL_job_add2 (ctx, 627 eh, 628 pch->ctx.headers, 629 &handle_purse_create_deposit_finished, 630 pch); 631 return pch; 632 } 633 634 635 void 636 TALER_EXCHANGE_purse_create_with_deposit_cancel ( 637 struct TALER_EXCHANGE_PurseCreateDepositHandle *pch) 638 { 639 if (NULL != pch->job) 640 { 641 GNUNET_CURL_job_cancel (pch->job); 642 pch->job = NULL; 643 } 644 GNUNET_free (pch->econtract.econtract); 645 GNUNET_free (pch->exchange_url); 646 GNUNET_free (pch->url); 647 GNUNET_array_grow (pch->deposits, 648 pch->num_deposits, 649 0); 650 TALER_EXCHANGE_keys_decref (pch->keys); 651 TALER_curl_easy_post_finished (&pch->ctx); 652 GNUNET_free (pch); 653 } 654 655 656 /* end of exchange_api_purse_create_with_deposit.c */