exchange_api_post-purses-PURSE_PUB-deposit.c (18009B)
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_INTERNAL_SERVER_ERROR: 374 dr.hr.ec = TALER_JSON_get_error_code (j); 375 /* Server had an internal issue; we should retry, but this API 376 leaves this to the application */ 377 break; 378 default: 379 /* unexpected response code */ 380 dr.hr.ec = TALER_JSON_get_error_code (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 (TALER_EC_NONE == dr.hr.ec) 389 dr.hr.hint = NULL; 390 else 391 dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec); 392 pch->cb (pch->cb_cls, 393 &dr); 394 TALER_EXCHANGE_free_coin_conflict_ (&dr.hr, 395 &dr.details.conflict); 396 TALER_EXCHANGE_post_purses_deposit_cancel (pch); 397 } 398 399 400 struct TALER_EXCHANGE_PostPursesDepositHandle * 401 TALER_EXCHANGE_post_purses_deposit_create ( 402 struct GNUNET_CURL_Context *ctx, 403 const char *url, 404 struct TALER_EXCHANGE_Keys *keys, 405 const char *purse_exchange_url, // FIXME: turn into option! 406 const struct TALER_PurseContractPublicKeyP *purse_pub, 407 uint8_t min_age, // FIXME: turn into option! 408 unsigned int num_deposits, 409 const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits]) 410 { 411 struct TALER_EXCHANGE_PostPursesDepositHandle *pch; 412 json_t *deposit_arr; 413 414 // FIXME: use purse_exchange_url for wad transfers (#7271) 415 (void) purse_exchange_url; 416 if (0 == num_deposits) 417 { 418 GNUNET_break (0); 419 return NULL; 420 } 421 pch = GNUNET_new (struct TALER_EXCHANGE_PostPursesDepositHandle); 422 pch->ctx = ctx; 423 pch->base_url = GNUNET_strdup (url); 424 pch->keys = TALER_EXCHANGE_keys_incref (keys); 425 pch->purse_pub = *purse_pub; 426 pch->num_deposits = num_deposits; 427 pch->coins = GNUNET_new_array (num_deposits, 428 struct Coin); 429 // FIXME: move JSON construction into _start() function. 430 deposit_arr = json_array (); 431 GNUNET_assert (NULL != deposit_arr); 432 for (unsigned int i = 0; i<num_deposits; i++) 433 { 434 const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i]; 435 const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof; 436 struct Coin *coin = &pch->coins[i]; 437 json_t *jdeposit; 438 struct TALER_AgeCommitmentHashP *achp = NULL; 439 struct TALER_AgeAttestationP attest; 440 struct TALER_AgeAttestationP *attestp = NULL; 441 442 if (NULL != acp) 443 { 444 TALER_age_commitment_hash (&acp->commitment, 445 &coin->ahac); 446 achp = &coin->ahac; 447 if (GNUNET_OK != 448 TALER_age_commitment_attest (acp, 449 min_age, 450 &attest)) 451 { 452 GNUNET_break (0); 453 json_decref (deposit_arr); 454 GNUNET_free (pch->base_url); 455 GNUNET_free (pch->coins); 456 TALER_EXCHANGE_keys_decref (pch->keys); 457 GNUNET_free (pch); 458 return NULL; 459 } 460 attestp = &attest; 461 } 462 GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, 463 &coin->coin_pub.eddsa_pub); 464 coin->h_denom_pub = deposit->h_denom_pub; 465 coin->contribution = deposit->amount; 466 TALER_wallet_purse_deposit_sign ( 467 pch->base_url, 468 &pch->purse_pub, 469 &deposit->amount, 470 &coin->h_denom_pub, 471 &coin->ahac, 472 &deposit->coin_priv, 473 &coin->coin_sig); 474 jdeposit = GNUNET_JSON_PACK ( 475 GNUNET_JSON_pack_allow_null ( 476 GNUNET_JSON_pack_data_auto ("h_age_commitment", 477 achp)), 478 GNUNET_JSON_pack_allow_null ( 479 GNUNET_JSON_pack_data_auto ("age_attestation", 480 attestp)), 481 TALER_JSON_pack_amount ("amount", 482 &deposit->amount), 483 GNUNET_JSON_pack_data_auto ("denom_pub_hash", 484 &deposit->h_denom_pub), 485 TALER_JSON_pack_denom_sig ("ub_sig", 486 &deposit->denom_sig), 487 GNUNET_JSON_pack_data_auto ("coin_pub", 488 &coin->coin_pub), 489 GNUNET_JSON_pack_data_auto ("coin_sig", 490 &coin->coin_sig)); 491 GNUNET_assert (0 == 492 json_array_append_new (deposit_arr, 493 jdeposit)); 494 } 495 pch->body = GNUNET_JSON_PACK ( 496 GNUNET_JSON_pack_array_steal ("deposits", 497 deposit_arr)); 498 GNUNET_assert (NULL != pch->body); 499 return pch; 500 } 501 502 503 enum TALER_ErrorCode 504 TALER_EXCHANGE_post_purses_deposit_start ( 505 struct TALER_EXCHANGE_PostPursesDepositHandle *pch, 506 TALER_EXCHANGE_PostPursesDepositCallback cb, 507 TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls) 508 { 509 CURL *eh; 510 char arg_str[sizeof (pch->purse_pub) * 2 + 32]; 511 512 pch->cb = cb; 513 pch->cb_cls = cb_cls; 514 { 515 char pub_str[sizeof (pch->purse_pub) * 2]; 516 char *end; 517 518 end = GNUNET_STRINGS_data_to_string ( 519 &pch->purse_pub, 520 sizeof (pch->purse_pub), 521 pub_str, 522 sizeof (pub_str)); 523 *end = '\0'; 524 GNUNET_snprintf (arg_str, 525 sizeof (arg_str), 526 "purses/%s/deposit", 527 pub_str); 528 } 529 pch->url = TALER_url_join (pch->base_url, 530 arg_str, 531 NULL); 532 if (NULL == pch->url) 533 { 534 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 535 "Could not construct request URL.\n"); 536 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 537 } 538 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 539 "URL for purse deposit: `%s'\n", 540 pch->url); 541 eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); 542 if ( (NULL == eh) || 543 (GNUNET_OK != 544 TALER_curl_easy_post (&pch->post_ctx, 545 eh, 546 pch->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 pch->job = GNUNET_CURL_job_add2 (pch->ctx, 554 eh, 555 pch->post_ctx.headers, 556 &handle_purse_deposit_finished, 557 pch); 558 if (NULL == pch->job) 559 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 560 return TALER_EC_NONE; 561 } 562 563 564 void 565 TALER_EXCHANGE_post_purses_deposit_cancel ( 566 struct TALER_EXCHANGE_PostPursesDepositHandle *pch) 567 { 568 if (NULL != pch->job) 569 { 570 GNUNET_CURL_job_cancel (pch->job); 571 pch->job = NULL; 572 } 573 TALER_curl_easy_post_finished (&pch->post_ctx); 574 GNUNET_free (pch->base_url); 575 GNUNET_free (pch->url); 576 GNUNET_free (pch->coins); 577 json_decref (pch->body); 578 TALER_EXCHANGE_keys_decref (pch->keys); 579 GNUNET_free (pch); 580 } 581 582 583 /* end of exchange_api_post-purses-PURSE_PUB-deposit.c */