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