exchange_api_post-purses-PURSE_PUB-deposit.c (18192B)
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-purses-PURSE_PUB-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 <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_common.h" 30 #include "exchange_api_handle.h" 31 #include "taler/taler_signatures.h" 32 #include "exchange_api_curl_defaults.h" 33 34 35 /** 36 * Information we track per coin. 37 */ 38 struct Coin 39 { 40 /** 41 * Coin's public key. 42 */ 43 struct TALER_CoinSpendPublicKeyP coin_pub; 44 45 /** 46 * Signature made with the coin. 47 */ 48 struct TALER_CoinSpendSignatureP coin_sig; 49 50 /** 51 * Coin's denomination. 52 */ 53 struct TALER_DenominationHashP h_denom_pub; 54 55 /** 56 * Age restriction hash for the coin. 57 */ 58 struct TALER_AgeCommitmentHashP ahac; 59 60 /** 61 * How much did we say the coin contributed. 62 */ 63 struct TALER_Amount contribution; 64 }; 65 66 67 /** 68 * @brief A purse deposit handle 69 */ 70 struct TALER_EXCHANGE_PostPursesDepositHandle 71 { 72 73 /** 74 * Reference to the execution context. 75 */ 76 struct GNUNET_CURL_Context *ctx; 77 78 /** 79 * The base url of the exchange we are talking to. 80 */ 81 char *base_url; 82 83 /** 84 * The full URL for this request, set during _start. 85 */ 86 char *url; 87 88 /** 89 * Minor context that holds body and headers. 90 */ 91 struct TALER_CURL_PostContext post_ctx; 92 93 /** 94 * Handle for the request. 95 */ 96 struct GNUNET_CURL_Job *job; 97 98 /** 99 * Function to call with the result. 100 */ 101 TALER_EXCHANGE_PostPursesDepositCallback cb; 102 103 /** 104 * Closure for @a cb. 105 */ 106 TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls; 107 108 /** 109 * The keys of the exchange this request handle will use. 110 */ 111 struct TALER_EXCHANGE_Keys *keys; 112 113 /** 114 * Public key of the purse. 115 */ 116 struct TALER_PurseContractPublicKeyP purse_pub; 117 118 /** 119 * Array of @e num_deposits coins we are depositing. 120 */ 121 struct Coin *coins; 122 123 /** 124 * Number of coins we are depositing. 125 */ 126 unsigned int num_deposits; 127 128 /** 129 * Pre-built request body. 130 */ 131 json_t *body; 132 133 }; 134 135 136 /** 137 * Find the deposited coin @a coin_pub and how we used it. 138 * 139 * @param cls a `struct TALER_EXCHANGE_PostPursesDepositHandle *` 140 * @param coin_pub public key of the coin named in the conflict reply 141 * @param[out] h_denom_pub set to the hash of the denomination we used 142 * @param[out] h_age_commitment set to the age commitment hash we used, or NULL 143 * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours 144 */ 145 static enum GNUNET_GenericReturnValue 146 purse_deposit_coin_lookup (void *cls, 147 const struct TALER_CoinSpendPublicKeyP *coin_pub, 148 const struct TALER_DenominationHashP **h_denom_pub, 149 const struct TALER_AgeCommitmentHashP ** 150 h_age_commitment) 151 { 152 struct TALER_EXCHANGE_PostPursesDepositHandle *pch = cls; 153 154 for (unsigned int i = 0; i<pch->num_deposits; i++) 155 { 156 const struct Coin *coin = &pch->coins[i]; 157 158 if (0 != GNUNET_memcmp (coin_pub, 159 &coin->coin_pub)) 160 continue; 161 *h_denom_pub = &coin->h_denom_pub; 162 *h_age_commitment = GNUNET_is_zero (&coin->ahac) 163 ? NULL 164 : &coin->ahac; 165 return GNUNET_OK; 166 } 167 return GNUNET_NO; 168 } 169 170 171 /** 172 * Function called when we're done processing the 173 * HTTP /purses/$PID/deposit request. 174 * 175 * @param cls the `struct TALER_EXCHANGE_PostPursesDepositHandle` 176 * @param response_code HTTP response code, 0 on error 177 * @param response parsed JSON result, NULL on error 178 */ 179 static void 180 handle_purse_deposit_finished (void *cls, 181 long response_code, 182 const void *response) 183 { 184 struct TALER_EXCHANGE_PostPursesDepositHandle *pch = cls; 185 const json_t *j = response; 186 struct TALER_EXCHANGE_PostPursesDepositResponse dr = { 187 .hr.reply = j, 188 .hr.http_status = (unsigned int) response_code 189 }; 190 const struct TALER_EXCHANGE_Keys *keys = pch->keys; 191 192 pch->job = NULL; 193 switch (response_code) 194 { 195 case 0: 196 dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 197 break; 198 case MHD_HTTP_OK: 199 { 200 struct GNUNET_TIME_Timestamp etime; 201 struct GNUNET_JSON_Specification spec[] = { 202 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 203 &dr.details.ok.exchange_sig), 204 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 205 &dr.details.ok.exchange_pub), 206 GNUNET_JSON_spec_fixed_auto ("h_contract_terms", 207 &dr.details.ok.h_contract_terms), 208 GNUNET_JSON_spec_timestamp ("exchange_timestamp", 209 &etime), 210 GNUNET_JSON_spec_timestamp ("purse_expiration", 211 &dr.details.ok.purse_expiration), 212 TALER_JSON_spec_amount ("total_deposited", 213 keys->currency, 214 &dr.details.ok.total_deposited), 215 TALER_JSON_spec_amount ("purse_value_after_fees", 216 keys->currency, 217 &dr.details.ok.purse_value_after_fees), 218 GNUNET_JSON_spec_end () 219 }; 220 221 if (GNUNET_OK != 222 GNUNET_JSON_parse (j, 223 spec, 224 NULL, NULL)) 225 { 226 GNUNET_break_op (0); 227 dr.hr.http_status = 0; 228 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 229 break; 230 } 231 if (GNUNET_OK != 232 TALER_EXCHANGE_test_signing_key (keys, 233 &dr.details.ok.exchange_pub)) 234 { 235 GNUNET_break_op (0); 236 dr.hr.http_status = 0; 237 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID; 238 break; 239 } 240 if (GNUNET_OK != 241 TALER_exchange_online_purse_created_verify ( 242 etime, 243 dr.details.ok.purse_expiration, 244 &dr.details.ok.purse_value_after_fees, 245 &dr.details.ok.total_deposited, 246 &pch->purse_pub, 247 &dr.details.ok.h_contract_terms, 248 &dr.details.ok.exchange_pub, 249 &dr.details.ok.exchange_sig)) 250 { 251 GNUNET_break_op (0); 252 dr.hr.http_status = 0; 253 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID; 254 break; 255 } 256 } 257 break; 258 case MHD_HTTP_BAD_REQUEST: 259 /* This should never happen, either us or the exchange is buggy 260 (or API version conflict); just pass JSON reply to the application */ 261 dr.hr.ec = TALER_JSON_get_error_code (j); 262 break; 263 case MHD_HTTP_FORBIDDEN: 264 dr.hr.ec = TALER_JSON_get_error_code (j); 265 /* Nothing really to verify, exchange says one of the signatures is 266 invalid; as we checked them, this should never happen, we 267 should pass the JSON reply to the application */ 268 break; 269 case MHD_HTTP_NOT_FOUND: 270 dr.hr.ec = TALER_JSON_get_error_code (j); 271 /* Nothing really to verify, this should never 272 happen, we should pass the JSON reply to the application */ 273 break; 274 case MHD_HTTP_CONFLICT: 275 dr.hr.ec = TALER_JSON_get_error_code (j); 276 switch (dr.hr.ec) 277 { 278 case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA: 279 { 280 struct TALER_CoinSpendPublicKeyP coin_pub; 281 struct TALER_CoinSpendSignatureP coin_sig; 282 struct TALER_DenominationHashP h_denom_pub; 283 struct TALER_AgeCommitmentHashP phac; 284 bool found = false; 285 286 if (GNUNET_OK != 287 TALER_EXCHANGE_check_purse_coin_conflict_ ( 288 &pch->purse_pub, 289 pch->base_url, 290 j, 291 &h_denom_pub, 292 &phac, 293 &coin_pub, 294 &coin_sig)) 295 { 296 GNUNET_break_op (0); 297 dr.hr.http_status = 0; 298 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 299 break; 300 } 301 for (unsigned int i = 0; i<pch->num_deposits; i++) 302 { 303 struct Coin *coin = &pch->coins[i]; 304 if (0 != GNUNET_memcmp (&coin_pub, 305 &coin->coin_pub)) 306 continue; 307 if (0 != 308 GNUNET_memcmp (&coin->h_denom_pub, 309 &h_denom_pub)) 310 { 311 found = true; 312 break; 313 } 314 if (0 != 315 GNUNET_memcmp (&coin->ahac, 316 &phac)) 317 { 318 found = true; 319 break; 320 } 321 if (0 == GNUNET_memcmp (&coin_sig, 322 &coin->coin_sig)) 323 { 324 /* identical signature => not a conflict */ 325 continue; 326 } 327 found = true; 328 break; 329 } 330 if (! found) 331 { 332 GNUNET_break_op (0); 333 dr.hr.http_status = 0; 334 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 335 break; 336 } 337 /* meta data conflict is real! */ 338 break; 339 } 340 case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: 341 /* Nothing to check anymore here, proof needs to be 342 checked in the GET /coins/$COIN_PUB handler */ 343 break; 344 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: 345 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: 346 if (GNUNET_OK != 347 TALER_EXCHANGE_check_coin_conflict_ (pch->keys, 348 dr.hr.ec, 349 j, 350 &purse_deposit_coin_lookup, 351 pch, 352 &dr.details.conflict)) 353 { 354 GNUNET_break_op (0); 355 dr.hr.http_status = 0; 356 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 357 } 358 break; 359 default: 360 GNUNET_break_op (0); 361 dr.hr.http_status = 0; 362 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 363 break; 364 } /* ec switch */ 365 break; 366 case MHD_HTTP_GONE: 367 /* could happen if denomination was revoked or purse expired */ 368 /* Note: one might want to check /keys for revocation 369 signature here, alas tricky in case our /keys 370 is outdated => left to clients */ 371 dr.hr.ec = TALER_JSON_get_error_code (j); 372 break; 373 case MHD_HTTP_PRECONDITION_FAILED: 374 /* could happen if we were too early and the denomination 375 is not yet valid */ 376 dr.hr.ec = TALER_JSON_get_error_code (j); 377 break; 378 case MHD_HTTP_INTERNAL_SERVER_ERROR: 379 dr.hr.ec = TALER_JSON_get_error_code (j); 380 /* Server had an internal issue; we should retry, but this API 381 leaves this to the application */ 382 break; 383 default: 384 /* unexpected response code */ 385 dr.hr.ec = TALER_JSON_get_error_code (j); 386 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 387 "Unexpected response code %u/%d for exchange deposit\n", 388 (unsigned int) response_code, 389 dr.hr.ec); 390 GNUNET_break_op (0); 391 break; 392 } 393 if (TALER_EC_NONE == dr.hr.ec) 394 dr.hr.hint = NULL; 395 else 396 dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec); 397 pch->cb (pch->cb_cls, 398 &dr); 399 TALER_EXCHANGE_free_coin_conflict_ (&dr.hr, 400 &dr.details.conflict); 401 TALER_EXCHANGE_post_purses_deposit_cancel (pch); 402 } 403 404 405 struct TALER_EXCHANGE_PostPursesDepositHandle * 406 TALER_EXCHANGE_post_purses_deposit_create ( 407 struct GNUNET_CURL_Context *ctx, 408 const char *url, 409 struct TALER_EXCHANGE_Keys *keys, 410 const char *purse_exchange_url, // FIXME: turn into option! 411 const struct TALER_PurseContractPublicKeyP *purse_pub, 412 uint8_t min_age, // FIXME: turn into option! 413 unsigned int num_deposits, 414 const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits]) 415 { 416 struct TALER_EXCHANGE_PostPursesDepositHandle *pch; 417 json_t *deposit_arr; 418 419 // FIXME: use purse_exchange_url for wad transfers (#7271) 420 (void) purse_exchange_url; 421 if (0 == num_deposits) 422 { 423 GNUNET_break (0); 424 return NULL; 425 } 426 pch = GNUNET_new (struct TALER_EXCHANGE_PostPursesDepositHandle); 427 pch->ctx = ctx; 428 pch->base_url = GNUNET_strdup (url); 429 pch->keys = TALER_EXCHANGE_keys_incref (keys); 430 pch->purse_pub = *purse_pub; 431 pch->num_deposits = num_deposits; 432 pch->coins = GNUNET_new_array (num_deposits, 433 struct Coin); 434 // FIXME: move JSON construction into _start() function. 435 deposit_arr = json_array (); 436 GNUNET_assert (NULL != deposit_arr); 437 for (unsigned int i = 0; i<num_deposits; i++) 438 { 439 const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i]; 440 const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof; 441 struct Coin *coin = &pch->coins[i]; 442 json_t *jdeposit; 443 struct TALER_AgeCommitmentHashP *achp = NULL; 444 struct TALER_AgeAttestationP attest; 445 struct TALER_AgeAttestationP *attestp = NULL; 446 447 if (NULL != acp) 448 { 449 TALER_age_commitment_hash (&acp->commitment, 450 &coin->ahac); 451 achp = &coin->ahac; 452 if (GNUNET_OK != 453 TALER_age_commitment_attest (acp, 454 min_age, 455 &attest)) 456 { 457 GNUNET_break (0); 458 json_decref (deposit_arr); 459 GNUNET_free (pch->base_url); 460 GNUNET_free (pch->coins); 461 TALER_EXCHANGE_keys_decref (pch->keys); 462 GNUNET_free (pch); 463 return NULL; 464 } 465 attestp = &attest; 466 } 467 GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, 468 &coin->coin_pub.eddsa_pub); 469 coin->h_denom_pub = deposit->h_denom_pub; 470 coin->contribution = deposit->amount; 471 TALER_wallet_purse_deposit_sign ( 472 pch->base_url, 473 &pch->purse_pub, 474 &deposit->amount, 475 &coin->h_denom_pub, 476 &coin->ahac, 477 &deposit->coin_priv, 478 &coin->coin_sig); 479 jdeposit = GNUNET_JSON_PACK ( 480 GNUNET_JSON_pack_allow_null ( 481 GNUNET_JSON_pack_data_auto ("h_age_commitment", 482 achp)), 483 GNUNET_JSON_pack_allow_null ( 484 GNUNET_JSON_pack_data_auto ("age_attestation", 485 attestp)), 486 TALER_JSON_pack_amount ("amount", 487 &deposit->amount), 488 GNUNET_JSON_pack_data_auto ("denom_pub_hash", 489 &deposit->h_denom_pub), 490 TALER_JSON_pack_denom_sig ("ub_sig", 491 &deposit->denom_sig), 492 GNUNET_JSON_pack_data_auto ("coin_pub", 493 &coin->coin_pub), 494 GNUNET_JSON_pack_data_auto ("coin_sig", 495 &coin->coin_sig)); 496 GNUNET_assert (0 == 497 json_array_append_new (deposit_arr, 498 jdeposit)); 499 } 500 pch->body = GNUNET_JSON_PACK ( 501 GNUNET_JSON_pack_array_steal ("deposits", 502 deposit_arr)); 503 GNUNET_assert (NULL != pch->body); 504 return pch; 505 } 506 507 508 enum TALER_ErrorCode 509 TALER_EXCHANGE_post_purses_deposit_start ( 510 struct TALER_EXCHANGE_PostPursesDepositHandle *pch, 511 TALER_EXCHANGE_PostPursesDepositCallback cb, 512 TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls) 513 { 514 CURL *eh; 515 char arg_str[sizeof (pch->purse_pub) * 2 + 32]; 516 517 pch->cb = cb; 518 pch->cb_cls = cb_cls; 519 { 520 char pub_str[sizeof (pch->purse_pub) * 2]; 521 char *end; 522 523 end = GNUNET_STRINGS_data_to_string ( 524 &pch->purse_pub, 525 sizeof (pch->purse_pub), 526 pub_str, 527 sizeof (pub_str)); 528 *end = '\0'; 529 GNUNET_snprintf (arg_str, 530 sizeof (arg_str), 531 "purses/%s/deposit", 532 pub_str); 533 } 534 pch->url = TALER_url_join (pch->base_url, 535 arg_str, 536 NULL); 537 if (NULL == pch->url) 538 { 539 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 540 "Could not construct request URL.\n"); 541 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 542 } 543 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 544 "URL for purse deposit: `%s'\n", 545 pch->url); 546 eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); 547 if ( (NULL == eh) || 548 (GNUNET_OK != 549 TALER_curl_easy_post (&pch->post_ctx, 550 eh, 551 pch->body)) ) 552 { 553 GNUNET_break (0); 554 if (NULL != eh) 555 curl_easy_cleanup (eh); 556 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 557 } 558 pch->job = GNUNET_CURL_job_add2 (pch->ctx, 559 eh, 560 pch->post_ctx.headers, 561 &handle_purse_deposit_finished, 562 pch); 563 if (NULL == pch->job) 564 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 565 return TALER_EC_NONE; 566 } 567 568 569 void 570 TALER_EXCHANGE_post_purses_deposit_cancel ( 571 struct TALER_EXCHANGE_PostPursesDepositHandle *pch) 572 { 573 if (NULL != pch->job) 574 { 575 GNUNET_CURL_job_cancel (pch->job); 576 pch->job = NULL; 577 } 578 TALER_curl_easy_post_finished (&pch->post_ctx); 579 GNUNET_free (pch->base_url); 580 GNUNET_free (pch->url); 581 GNUNET_free (pch->coins); 582 json_decref (pch->body); 583 TALER_EXCHANGE_keys_decref (pch->keys); 584 GNUNET_free (pch); 585 } 586 587 588 /* end of exchange_api_post-purses-PURSE_PUB-deposit.c */