exchange_api_post-purses-PURSE_PUB-deposit.c (16114B)
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 * Function called when we're done processing the 138 * HTTP /purses/$PID/deposit request. 139 * 140 * @param cls the `struct TALER_EXCHANGE_PostPursesDepositHandle` 141 * @param response_code HTTP response code, 0 on error 142 * @param response parsed JSON result, NULL on error 143 */ 144 static void 145 handle_purse_deposit_finished (void *cls, 146 long response_code, 147 const void *response) 148 { 149 struct TALER_EXCHANGE_PostPursesDepositHandle *pch = cls; 150 const json_t *j = response; 151 struct TALER_EXCHANGE_PostPursesDepositResponse dr = { 152 .hr.reply = j, 153 .hr.http_status = (unsigned int) response_code 154 }; 155 const struct TALER_EXCHANGE_Keys *keys = pch->keys; 156 157 pch->job = NULL; 158 switch (response_code) 159 { 160 case 0: 161 dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 162 break; 163 case MHD_HTTP_OK: 164 { 165 struct GNUNET_TIME_Timestamp etime; 166 struct GNUNET_JSON_Specification spec[] = { 167 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 168 &dr.details.ok.exchange_sig), 169 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 170 &dr.details.ok.exchange_pub), 171 GNUNET_JSON_spec_fixed_auto ("h_contract_terms", 172 &dr.details.ok.h_contract_terms), 173 GNUNET_JSON_spec_timestamp ("exchange_timestamp", 174 &etime), 175 GNUNET_JSON_spec_timestamp ("purse_expiration", 176 &dr.details.ok.purse_expiration), 177 TALER_JSON_spec_amount ("total_deposited", 178 keys->currency, 179 &dr.details.ok.total_deposited), 180 TALER_JSON_spec_amount ("purse_value_after_fees", 181 keys->currency, 182 &dr.details.ok.purse_value_after_fees), 183 GNUNET_JSON_spec_end () 184 }; 185 186 if (GNUNET_OK != 187 GNUNET_JSON_parse (j, 188 spec, 189 NULL, NULL)) 190 { 191 GNUNET_break_op (0); 192 dr.hr.http_status = 0; 193 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 194 break; 195 } 196 if (GNUNET_OK != 197 TALER_EXCHANGE_test_signing_key (keys, 198 &dr.details.ok.exchange_pub)) 199 { 200 GNUNET_break_op (0); 201 dr.hr.http_status = 0; 202 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID; 203 break; 204 } 205 if (GNUNET_OK != 206 TALER_exchange_online_purse_created_verify ( 207 etime, 208 dr.details.ok.purse_expiration, 209 &dr.details.ok.purse_value_after_fees, 210 &dr.details.ok.total_deposited, 211 &pch->purse_pub, 212 &dr.details.ok.h_contract_terms, 213 &dr.details.ok.exchange_pub, 214 &dr.details.ok.exchange_sig)) 215 { 216 GNUNET_break_op (0); 217 dr.hr.http_status = 0; 218 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID; 219 break; 220 } 221 } 222 break; 223 case MHD_HTTP_BAD_REQUEST: 224 /* This should never happen, either us or the exchange is buggy 225 (or API version conflict); just pass JSON reply to the application */ 226 dr.hr.ec = TALER_JSON_get_error_code (j); 227 break; 228 case MHD_HTTP_FORBIDDEN: 229 dr.hr.ec = TALER_JSON_get_error_code (j); 230 /* Nothing really to verify, exchange says one of the signatures is 231 invalid; as we checked them, this should never happen, we 232 should pass the JSON reply to the application */ 233 break; 234 case MHD_HTTP_NOT_FOUND: 235 dr.hr.ec = TALER_JSON_get_error_code (j); 236 /* Nothing really to verify, this should never 237 happen, we should pass the JSON reply to the application */ 238 break; 239 case MHD_HTTP_CONFLICT: 240 dr.hr.ec = TALER_JSON_get_error_code (j); 241 switch (dr.hr.ec) 242 { 243 case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA: 244 { 245 struct TALER_CoinSpendPublicKeyP coin_pub; 246 struct TALER_CoinSpendSignatureP coin_sig; 247 struct TALER_DenominationHashP h_denom_pub; 248 struct TALER_AgeCommitmentHashP phac; 249 bool found = false; 250 251 if (GNUNET_OK != 252 TALER_EXCHANGE_check_purse_coin_conflict_ ( 253 &pch->purse_pub, 254 pch->base_url, 255 j, 256 &h_denom_pub, 257 &phac, 258 &coin_pub, 259 &coin_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 for (unsigned int i = 0; i<pch->num_deposits; i++) 267 { 268 struct Coin *coin = &pch->coins[i]; 269 if (0 != GNUNET_memcmp (&coin_pub, 270 &coin->coin_pub)) 271 continue; 272 if (0 != 273 GNUNET_memcmp (&coin->h_denom_pub, 274 &h_denom_pub)) 275 { 276 found = true; 277 break; 278 } 279 if (0 != 280 GNUNET_memcmp (&coin->ahac, 281 &phac)) 282 { 283 found = true; 284 break; 285 } 286 if (0 == GNUNET_memcmp (&coin_sig, 287 &coin->coin_sig)) 288 { 289 /* identical signature => not a conflict */ 290 continue; 291 } 292 found = true; 293 break; 294 } 295 if (! found) 296 { 297 GNUNET_break_op (0); 298 dr.hr.http_status = 0; 299 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 300 break; 301 } 302 /* meta data conflict is real! */ 303 break; 304 } 305 case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: 306 /* Nothing to check anymore here, proof needs to be 307 checked in the GET /coins/$COIN_PUB handler */ 308 break; 309 case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: 310 break; 311 default: 312 GNUNET_break_op (0); 313 dr.hr.http_status = 0; 314 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 315 break; 316 } /* ec switch */ 317 break; 318 case MHD_HTTP_GONE: 319 /* could happen if denomination was revoked or purse expired */ 320 /* Note: one might want to check /keys for revocation 321 signature here, alas tricky in case our /keys 322 is outdated => left to clients */ 323 dr.hr.ec = TALER_JSON_get_error_code (j); 324 break; 325 case MHD_HTTP_INTERNAL_SERVER_ERROR: 326 dr.hr.ec = TALER_JSON_get_error_code (j); 327 /* Server had an internal issue; we should retry, but this API 328 leaves this to the application */ 329 break; 330 default: 331 /* unexpected response code */ 332 dr.hr.ec = TALER_JSON_get_error_code (j); 333 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 334 "Unexpected response code %u/%d for exchange deposit\n", 335 (unsigned int) response_code, 336 dr.hr.ec); 337 GNUNET_break_op (0); 338 break; 339 } 340 if (TALER_EC_NONE == dr.hr.ec) 341 dr.hr.hint = NULL; 342 else 343 dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec); 344 pch->cb (pch->cb_cls, 345 &dr); 346 TALER_EXCHANGE_post_purses_deposit_cancel (pch); 347 } 348 349 350 struct TALER_EXCHANGE_PostPursesDepositHandle * 351 TALER_EXCHANGE_post_purses_deposit_create ( 352 struct GNUNET_CURL_Context *ctx, 353 const char *url, 354 struct TALER_EXCHANGE_Keys *keys, 355 const char *purse_exchange_url, // FIXME: turn into option! 356 const struct TALER_PurseContractPublicKeyP *purse_pub, 357 uint8_t min_age, // FIXME: turn into option! 358 unsigned int num_deposits, 359 const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits]) 360 { 361 struct TALER_EXCHANGE_PostPursesDepositHandle *pch; 362 json_t *deposit_arr; 363 364 // FIXME: use purse_exchange_url for wad transfers (#7271) 365 (void) purse_exchange_url; 366 if (0 == num_deposits) 367 { 368 GNUNET_break (0); 369 return NULL; 370 } 371 pch = GNUNET_new (struct TALER_EXCHANGE_PostPursesDepositHandle); 372 pch->ctx = ctx; 373 pch->base_url = GNUNET_strdup (url); 374 pch->keys = TALER_EXCHANGE_keys_incref (keys); 375 pch->purse_pub = *purse_pub; 376 pch->num_deposits = num_deposits; 377 pch->coins = GNUNET_new_array (num_deposits, 378 struct Coin); 379 // FIXME: move JSON construction into _start() function. 380 deposit_arr = json_array (); 381 GNUNET_assert (NULL != deposit_arr); 382 for (unsigned int i = 0; i<num_deposits; i++) 383 { 384 const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i]; 385 const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof; 386 struct Coin *coin = &pch->coins[i]; 387 json_t *jdeposit; 388 struct TALER_AgeCommitmentHashP *achp = NULL; 389 struct TALER_AgeAttestationP attest; 390 struct TALER_AgeAttestationP *attestp = NULL; 391 392 if (NULL != acp) 393 { 394 TALER_age_commitment_hash (&acp->commitment, 395 &coin->ahac); 396 achp = &coin->ahac; 397 if (GNUNET_OK != 398 TALER_age_commitment_attest (acp, 399 min_age, 400 &attest)) 401 { 402 GNUNET_break (0); 403 json_decref (deposit_arr); 404 GNUNET_free (pch->base_url); 405 GNUNET_free (pch->coins); 406 TALER_EXCHANGE_keys_decref (pch->keys); 407 GNUNET_free (pch); 408 return NULL; 409 } 410 attestp = &attest; 411 } 412 GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, 413 &coin->coin_pub.eddsa_pub); 414 coin->h_denom_pub = deposit->h_denom_pub; 415 coin->contribution = deposit->amount; 416 TALER_wallet_purse_deposit_sign ( 417 pch->base_url, 418 &pch->purse_pub, 419 &deposit->amount, 420 &coin->h_denom_pub, 421 &coin->ahac, 422 &deposit->coin_priv, 423 &coin->coin_sig); 424 jdeposit = GNUNET_JSON_PACK ( 425 GNUNET_JSON_pack_allow_null ( 426 GNUNET_JSON_pack_data_auto ("h_age_commitment", 427 achp)), 428 GNUNET_JSON_pack_allow_null ( 429 GNUNET_JSON_pack_data_auto ("age_attestation", 430 attestp)), 431 TALER_JSON_pack_amount ("amount", 432 &deposit->amount), 433 GNUNET_JSON_pack_data_auto ("denom_pub_hash", 434 &deposit->h_denom_pub), 435 TALER_JSON_pack_denom_sig ("ub_sig", 436 &deposit->denom_sig), 437 GNUNET_JSON_pack_data_auto ("coin_pub", 438 &coin->coin_pub), 439 GNUNET_JSON_pack_data_auto ("coin_sig", 440 &coin->coin_sig)); 441 GNUNET_assert (0 == 442 json_array_append_new (deposit_arr, 443 jdeposit)); 444 } 445 pch->body = GNUNET_JSON_PACK ( 446 GNUNET_JSON_pack_array_steal ("deposits", 447 deposit_arr)); 448 GNUNET_assert (NULL != pch->body); 449 return pch; 450 } 451 452 453 enum TALER_ErrorCode 454 TALER_EXCHANGE_post_purses_deposit_start ( 455 struct TALER_EXCHANGE_PostPursesDepositHandle *pch, 456 TALER_EXCHANGE_PostPursesDepositCallback cb, 457 TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls) 458 { 459 CURL *eh; 460 char arg_str[sizeof (pch->purse_pub) * 2 + 32]; 461 462 pch->cb = cb; 463 pch->cb_cls = cb_cls; 464 { 465 char pub_str[sizeof (pch->purse_pub) * 2]; 466 char *end; 467 468 end = GNUNET_STRINGS_data_to_string ( 469 &pch->purse_pub, 470 sizeof (pch->purse_pub), 471 pub_str, 472 sizeof (pub_str)); 473 *end = '\0'; 474 GNUNET_snprintf (arg_str, 475 sizeof (arg_str), 476 "purses/%s/deposit", 477 pub_str); 478 } 479 pch->url = TALER_url_join (pch->base_url, 480 arg_str, 481 NULL); 482 if (NULL == pch->url) 483 { 484 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 485 "Could not construct request URL.\n"); 486 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 487 } 488 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 489 "URL for purse deposit: `%s'\n", 490 pch->url); 491 eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); 492 if ( (NULL == eh) || 493 (GNUNET_OK != 494 TALER_curl_easy_post (&pch->post_ctx, 495 eh, 496 pch->body)) ) 497 { 498 GNUNET_break (0); 499 if (NULL != eh) 500 curl_easy_cleanup (eh); 501 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 502 } 503 pch->job = GNUNET_CURL_job_add2 (pch->ctx, 504 eh, 505 pch->post_ctx.headers, 506 &handle_purse_deposit_finished, 507 pch); 508 if (NULL == pch->job) 509 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 510 return TALER_EC_NONE; 511 } 512 513 514 void 515 TALER_EXCHANGE_post_purses_deposit_cancel ( 516 struct TALER_EXCHANGE_PostPursesDepositHandle *pch) 517 { 518 if (NULL != pch->job) 519 { 520 GNUNET_CURL_job_cancel (pch->job); 521 pch->job = NULL; 522 } 523 TALER_curl_easy_post_finished (&pch->post_ctx); 524 GNUNET_free (pch->base_url); 525 GNUNET_free (pch->url); 526 GNUNET_free (pch->coins); 527 json_decref (pch->body); 528 TALER_EXCHANGE_keys_decref (pch->keys); 529 GNUNET_free (pch); 530 } 531 532 533 /* end of exchange_api_post-purses-PURSE_PUB-deposit.c */