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