exchange_api_purse_create_with_merge.c (18008B)
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_create_with_merge.c 19 * @brief Implementation of the client to create a 20 * purse for an account 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_handle.h" 32 #include "exchange_api_common.h" 33 #include "taler/taler_signatures.h" 34 #include "exchange_api_curl_defaults.h" 35 36 37 /** 38 * @brief A purse create with merge handle 39 */ 40 struct TALER_EXCHANGE_PurseCreateMergeHandle 41 { 42 43 /** 44 * The keys of the exchange this request handle will use 45 */ 46 struct TALER_EXCHANGE_Keys *keys; 47 48 /** 49 * The url for this request. 50 */ 51 char *url; 52 53 /** 54 * The exchange base URL. 55 */ 56 char *exchange_url; 57 58 /** 59 * Context for #TEH_curl_easy_post(). Keeps the data that must 60 * persist for Curl to make the upload. 61 */ 62 struct TALER_CURL_PostContext ctx; 63 64 /** 65 * Handle for the request. 66 */ 67 struct GNUNET_CURL_Job *job; 68 69 /** 70 * Function to call with the result. 71 */ 72 TALER_EXCHANGE_PurseCreateMergeCallback cb; 73 74 /** 75 * Closure for @a cb. 76 */ 77 void *cb_cls; 78 79 /** 80 * The encrypted contract (if any). 81 */ 82 struct TALER_EncryptedContract econtract; 83 84 /** 85 * Expected value in the purse after fees. 86 */ 87 struct TALER_Amount purse_value_after_fees; 88 89 /** 90 * Public key of the reserve public key. 91 */ 92 struct TALER_ReservePublicKeyP reserve_pub; 93 94 /** 95 * Reserve signature affirming our merge. 96 */ 97 struct TALER_ReserveSignatureP reserve_sig; 98 99 /** 100 * Merge capability key. 101 */ 102 struct TALER_PurseMergePublicKeyP merge_pub; 103 104 /** 105 * Our merge signature (if any). 106 */ 107 struct TALER_PurseMergeSignatureP merge_sig; 108 109 /** 110 * Public key of the purse. 111 */ 112 struct TALER_PurseContractPublicKeyP purse_pub; 113 114 /** 115 * Request data we signed over. 116 */ 117 struct TALER_PurseContractSignatureP purse_sig; 118 119 /** 120 * Hash over the purse's contrac terms. 121 */ 122 struct TALER_PrivateContractHashP h_contract_terms; 123 124 /** 125 * When does the purse expire. 126 */ 127 struct GNUNET_TIME_Timestamp purse_expiration; 128 129 /** 130 * When does the purse get merged/created. 131 */ 132 struct GNUNET_TIME_Timestamp merge_timestamp; 133 }; 134 135 136 /** 137 * Function called when we're done processing the 138 * HTTP /reserves/$RID/purse request. 139 * 140 * @param cls the `struct TALER_EXCHANGE_PurseCreateMergeHandle` 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_create_with_merge_finished (void *cls, 146 long response_code, 147 const void *response) 148 { 149 struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm = cls; 150 const json_t *j = response; 151 struct TALER_EXCHANGE_PurseCreateMergeResponse dr = { 152 .hr.reply = j, 153 .hr.http_status = (unsigned int) response_code, 154 .reserve_sig = &pcm->reserve_sig 155 }; 156 157 pcm->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 TALER_Amount total_deposited; 167 struct TALER_ExchangeSignatureP exchange_sig; 168 struct TALER_ExchangePublicKeyP exchange_pub; 169 struct GNUNET_JSON_Specification spec[] = { 170 TALER_JSON_spec_amount_any ("total_deposited", 171 &total_deposited), 172 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 173 &exchange_sig), 174 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 175 &exchange_pub), 176 GNUNET_JSON_spec_timestamp ("exchange_timestamp", 177 &etime), 178 GNUNET_JSON_spec_end () 179 }; 180 181 if (GNUNET_OK != 182 GNUNET_JSON_parse (j, 183 spec, 184 NULL, NULL)) 185 { 186 GNUNET_break_op (0); 187 dr.hr.http_status = 0; 188 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 189 break; 190 } 191 if (GNUNET_OK != 192 TALER_EXCHANGE_test_signing_key (pcm->keys, 193 &exchange_pub)) 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_online_purse_created_verify ( 202 etime, 203 pcm->purse_expiration, 204 &pcm->purse_value_after_fees, 205 &total_deposited, 206 &pcm->purse_pub, 207 &pcm->h_contract_terms, 208 &exchange_pub, 209 &exchange_sig)) 210 { 211 GNUNET_break_op (0); 212 dr.hr.http_status = 0; 213 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 214 break; 215 } 216 } 217 break; 218 case MHD_HTTP_BAD_REQUEST: 219 /* This should never happen, either us or the exchange is buggy 220 (or API version conflict); just pass JSON reply to the application */ 221 dr.hr.ec = TALER_JSON_get_error_code (j); 222 dr.hr.hint = TALER_JSON_get_error_hint (j); 223 break; 224 case MHD_HTTP_FORBIDDEN: 225 dr.hr.ec = TALER_JSON_get_error_code (j); 226 dr.hr.hint = TALER_JSON_get_error_hint (j); 227 /* Nothing really to verify, exchange says one of the signatures is 228 invalid; as we checked them, this should never happen, we 229 should pass the JSON reply to the application */ 230 break; 231 case MHD_HTTP_NOT_FOUND: 232 dr.hr.ec = TALER_JSON_get_error_code (j); 233 dr.hr.hint = TALER_JSON_get_error_hint (j); 234 /* Nothing really to verify, this should never 235 happen, we should pass the JSON reply to the application */ 236 break; 237 case MHD_HTTP_CONFLICT: 238 dr.hr.ec = TALER_JSON_get_error_code (j); 239 switch (dr.hr.ec) 240 { 241 case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA: 242 if (GNUNET_OK != 243 TALER_EXCHANGE_check_purse_create_conflict_ ( 244 &pcm->purse_sig, 245 &pcm->purse_pub, 246 j)) 247 { 248 dr.hr.http_status = 0; 249 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 250 break; 251 } 252 break; 253 case TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA: 254 if (GNUNET_OK != 255 TALER_EXCHANGE_check_purse_merge_conflict_ ( 256 &pcm->merge_sig, 257 &pcm->merge_pub, 258 &pcm->purse_pub, 259 pcm->exchange_url, 260 j)) 261 { 262 GNUNET_break_op (0); 263 dr.hr.http_status = 0; 264 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 265 break; 266 } 267 break; 268 case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS: 269 /* nothing to verify */ 270 break; 271 case TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA: 272 if (GNUNET_OK != 273 TALER_EXCHANGE_check_purse_econtract_conflict_ ( 274 &pcm->econtract.econtract_sig, 275 &pcm->purse_pub, 276 j)) 277 { 278 GNUNET_break_op (0); 279 dr.hr.http_status = 0; 280 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 281 break; 282 } 283 break; 284 default: 285 /* unexpected EC! */ 286 GNUNET_break_op (0); 287 dr.hr.http_status = 0; 288 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 289 break; 290 } /* end inner (EC) switch */ 291 break; 292 case MHD_HTTP_GONE: 293 /* could happen if denomination was revoked */ 294 /* Note: one might want to check /keys for revocation 295 signature here, alas tricky in case our /keys 296 is outdated => left to clients */ 297 dr.hr.ec = TALER_JSON_get_error_code (j); 298 dr.hr.hint = TALER_JSON_get_error_hint (j); 299 break; 300 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 301 { 302 struct GNUNET_JSON_Specification spec[] = { 303 GNUNET_JSON_spec_uint64 ( 304 "requirement_row", 305 &dr.details.unavailable_for_legal_reasons.requirement_row), 306 GNUNET_JSON_spec_end () 307 }; 308 309 if (GNUNET_OK != 310 GNUNET_JSON_parse (j, 311 spec, 312 NULL, NULL)) 313 { 314 GNUNET_break_op (0); 315 dr.hr.http_status = 0; 316 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 317 break; 318 } 319 } 320 break; 321 case MHD_HTTP_INTERNAL_SERVER_ERROR: 322 dr.hr.ec = TALER_JSON_get_error_code (j); 323 dr.hr.hint = TALER_JSON_get_error_hint (j); 324 /* Server had an internal issue; we should retry, but this API 325 leaves this to the application */ 326 break; 327 default: 328 /* unexpected response code */ 329 dr.hr.ec = TALER_JSON_get_error_code (j); 330 dr.hr.hint = TALER_JSON_get_error_hint (j); 331 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 332 "Unexpected response code %u/%d for exchange deposit\n", 333 (unsigned int) response_code, 334 dr.hr.ec); 335 GNUNET_break_op (0); 336 break; 337 } 338 pcm->cb (pcm->cb_cls, 339 &dr); 340 TALER_EXCHANGE_purse_create_with_merge_cancel (pcm); 341 } 342 343 344 struct TALER_EXCHANGE_PurseCreateMergeHandle * 345 TALER_EXCHANGE_purse_create_with_merge ( 346 struct GNUNET_CURL_Context *ctx, 347 const char *url, 348 struct TALER_EXCHANGE_Keys *keys, 349 const struct TALER_ReservePrivateKeyP *reserve_priv, 350 const struct TALER_PurseContractPrivateKeyP *purse_priv, 351 const struct TALER_PurseMergePrivateKeyP *merge_priv, 352 const struct TALER_ContractDiffiePrivateP *contract_priv, 353 const json_t *contract_terms, 354 bool upload_contract, 355 bool pay_for_purse, 356 struct GNUNET_TIME_Timestamp merge_timestamp, 357 TALER_EXCHANGE_PurseCreateMergeCallback cb, 358 void *cb_cls) 359 { 360 struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm; 361 json_t *create_with_merge_obj; 362 CURL *eh; 363 char arg_str[sizeof (pcm->reserve_pub) * 2 + 32]; 364 uint32_t min_age = 0; 365 struct TALER_Amount purse_fee; 366 enum TALER_WalletAccountMergeFlags flags; 367 368 pcm = GNUNET_new (struct TALER_EXCHANGE_PurseCreateMergeHandle); 369 pcm->cb = cb; 370 pcm->cb_cls = cb_cls; 371 if (GNUNET_OK != 372 TALER_JSON_contract_hash (contract_terms, 373 &pcm->h_contract_terms)) 374 { 375 GNUNET_break (0); 376 GNUNET_free (pcm); 377 return NULL; 378 } 379 pcm->merge_timestamp = merge_timestamp; 380 GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv, 381 &pcm->purse_pub.eddsa_pub); 382 GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, 383 &pcm->reserve_pub.eddsa_pub); 384 GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv, 385 &pcm->merge_pub.eddsa_pub); 386 387 { 388 struct GNUNET_JSON_Specification spec[] = { 389 TALER_JSON_spec_amount_any ("amount", 390 &pcm->purse_value_after_fees), 391 GNUNET_JSON_spec_mark_optional ( 392 GNUNET_JSON_spec_uint32 ("minimum_age", 393 &min_age), 394 NULL), 395 GNUNET_JSON_spec_timestamp ("pay_deadline", 396 &pcm->purse_expiration), 397 GNUNET_JSON_spec_end () 398 }; 399 400 if (GNUNET_OK != 401 GNUNET_JSON_parse (contract_terms, 402 spec, 403 NULL, NULL)) 404 { 405 GNUNET_break (0); 406 GNUNET_free (pcm); 407 return NULL; 408 } 409 } 410 if (pay_for_purse) 411 { 412 const struct TALER_EXCHANGE_GlobalFee *gf; 413 414 gf = TALER_EXCHANGE_get_global_fee ( 415 keys, 416 GNUNET_TIME_timestamp_get ()); 417 purse_fee = gf->fees.purse; 418 flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; 419 } 420 else 421 { 422 GNUNET_assert (GNUNET_OK == 423 TALER_amount_set_zero (pcm->purse_value_after_fees.currency, 424 &purse_fee)); 425 flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; 426 } 427 428 { 429 char pub_str[sizeof (pcm->reserve_pub) * 2]; 430 char *end; 431 432 end = GNUNET_STRINGS_data_to_string ( 433 &pcm->reserve_pub, 434 sizeof (pcm->reserve_pub), 435 pub_str, 436 sizeof (pub_str)); 437 *end = '\0'; 438 GNUNET_snprintf (arg_str, 439 sizeof (arg_str), 440 "reserves/%s/purse", 441 pub_str); 442 } 443 pcm->url = TALER_url_join (url, 444 arg_str, 445 NULL); 446 if (NULL == pcm->url) 447 { 448 GNUNET_break (0); 449 GNUNET_free (pcm); 450 return NULL; 451 } 452 TALER_wallet_purse_create_sign (pcm->purse_expiration, 453 &pcm->h_contract_terms, 454 &pcm->merge_pub, 455 min_age, 456 &pcm->purse_value_after_fees, 457 purse_priv, 458 &pcm->purse_sig); 459 { 460 struct TALER_NormalizedPayto payto_uri; 461 462 payto_uri = TALER_reserve_make_payto (url, 463 &pcm->reserve_pub); 464 TALER_wallet_purse_merge_sign (payto_uri, 465 merge_timestamp, 466 &pcm->purse_pub, 467 merge_priv, 468 &pcm->merge_sig); 469 GNUNET_free (payto_uri.normalized_payto); 470 } 471 TALER_wallet_account_merge_sign (merge_timestamp, 472 &pcm->purse_pub, 473 pcm->purse_expiration, 474 &pcm->h_contract_terms, 475 &pcm->purse_value_after_fees, 476 &purse_fee, 477 min_age, 478 flags, 479 reserve_priv, 480 &pcm->reserve_sig); 481 if (upload_contract) 482 { 483 TALER_CRYPTO_contract_encrypt_for_deposit ( 484 &pcm->purse_pub, 485 contract_priv, 486 contract_terms, 487 &pcm->econtract.econtract, 488 &pcm->econtract.econtract_size); 489 GNUNET_CRYPTO_ecdhe_key_get_public (&contract_priv->ecdhe_priv, 490 &pcm->econtract.contract_pub.ecdhe_pub); 491 TALER_wallet_econtract_upload_sign ( 492 pcm->econtract.econtract, 493 pcm->econtract.econtract_size, 494 &pcm->econtract.contract_pub, 495 purse_priv, 496 &pcm->econtract.econtract_sig); 497 } 498 create_with_merge_obj = GNUNET_JSON_PACK ( 499 TALER_JSON_pack_amount ("purse_value", 500 &pcm->purse_value_after_fees), 501 GNUNET_JSON_pack_uint64 ("min_age", 502 min_age), 503 GNUNET_JSON_pack_allow_null ( 504 TALER_JSON_pack_econtract ("econtract", 505 upload_contract 506 ? &pcm->econtract 507 : NULL)), 508 GNUNET_JSON_pack_allow_null ( 509 pay_for_purse 510 ? TALER_JSON_pack_amount ("purse_fee", 511 &purse_fee) 512 : GNUNET_JSON_pack_string ("dummy2", 513 NULL)), 514 GNUNET_JSON_pack_data_auto ("merge_pub", 515 &pcm->merge_pub), 516 GNUNET_JSON_pack_data_auto ("merge_sig", 517 &pcm->merge_sig), 518 GNUNET_JSON_pack_data_auto ("reserve_sig", 519 &pcm->reserve_sig), 520 GNUNET_JSON_pack_data_auto ("purse_pub", 521 &pcm->purse_pub), 522 GNUNET_JSON_pack_data_auto ("purse_sig", 523 &pcm->purse_sig), 524 GNUNET_JSON_pack_data_auto ("h_contract_terms", 525 &pcm->h_contract_terms), 526 GNUNET_JSON_pack_timestamp ("merge_timestamp", 527 merge_timestamp), 528 GNUNET_JSON_pack_timestamp ("purse_expiration", 529 pcm->purse_expiration)); 530 GNUNET_assert (NULL != create_with_merge_obj); 531 eh = TALER_EXCHANGE_curl_easy_get_ (pcm->url); 532 if ( (NULL == eh) || 533 (GNUNET_OK != 534 TALER_curl_easy_post (&pcm->ctx, 535 eh, 536 create_with_merge_obj)) ) 537 { 538 GNUNET_break (0); 539 if (NULL != eh) 540 curl_easy_cleanup (eh); 541 json_decref (create_with_merge_obj); 542 GNUNET_free (pcm->econtract.econtract); 543 GNUNET_free (pcm->url); 544 GNUNET_free (pcm); 545 return NULL; 546 } 547 json_decref (create_with_merge_obj); 548 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 549 "URL for purse create_with_merge: `%s'\n", 550 pcm->url); 551 pcm->keys = TALER_EXCHANGE_keys_incref (keys); 552 pcm->exchange_url = GNUNET_strdup (url); 553 pcm->job = GNUNET_CURL_job_add2 (ctx, 554 eh, 555 pcm->ctx.headers, 556 &handle_purse_create_with_merge_finished, 557 pcm); 558 return pcm; 559 } 560 561 562 void 563 TALER_EXCHANGE_purse_create_with_merge_cancel ( 564 struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm) 565 { 566 if (NULL != pcm->job) 567 { 568 GNUNET_CURL_job_cancel (pcm->job); 569 pcm->job = NULL; 570 } 571 GNUNET_free (pcm->url); 572 GNUNET_free (pcm->exchange_url); 573 TALER_curl_easy_post_finished (&pcm->ctx); 574 TALER_EXCHANGE_keys_decref (pcm->keys); 575 GNUNET_free (pcm->econtract.econtract); 576 GNUNET_free (pcm); 577 } 578 579 580 /* end of exchange_api_purse_create_with_merge.c */