exchange_api_purse_merge.c (13845B)
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_merge.c 19 * @brief Implementation of the client to merge a purse 20 * into 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 merge with deposit handle 39 */ 40 struct TALER_EXCHANGE_AccountMergeHandle 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 * Context for #TEH_curl_easy_post(). Keeps the data that must 55 * persist for Curl to make the upload. 56 */ 57 struct TALER_CURL_PostContext ctx; 58 59 /** 60 * Handle for the request. 61 */ 62 struct GNUNET_CURL_Job *job; 63 64 /** 65 * Function to call with the result. 66 */ 67 TALER_EXCHANGE_AccountMergeCallback cb; 68 69 /** 70 * Closure for @a cb. 71 */ 72 void *cb_cls; 73 74 /** 75 * Base URL of the provider hosting the @e reserve_pub. 76 */ 77 char *provider_url; 78 79 /** 80 * Signature for our operation. 81 */ 82 struct TALER_PurseMergeSignatureP merge_sig; 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 * Public key of the purse. 96 */ 97 struct TALER_PurseContractPublicKeyP purse_pub; 98 99 /** 100 * Hash over the purse's contrac terms. 101 */ 102 struct TALER_PrivateContractHashP h_contract_terms; 103 104 /** 105 * When does the purse expire. 106 */ 107 struct GNUNET_TIME_Timestamp purse_expiration; 108 109 /** 110 * Our merge key. 111 */ 112 struct TALER_PurseMergePrivateKeyP merge_priv; 113 114 /** 115 * Reserve signature affirming the merge. 116 */ 117 struct TALER_ReserveSignatureP reserve_sig; 118 119 }; 120 121 122 /** 123 * Function called when we're done processing the 124 * HTTP /purse/$PID/merge request. 125 * 126 * @param cls the `struct TALER_EXCHANGE_AccountMergeHandle` 127 * @param response_code HTTP response code, 0 on error 128 * @param response parsed JSON result, NULL on error 129 */ 130 static void 131 handle_purse_merge_finished (void *cls, 132 long response_code, 133 const void *response) 134 { 135 struct TALER_EXCHANGE_AccountMergeHandle *pch = cls; 136 const json_t *j = response; 137 struct TALER_EXCHANGE_AccountMergeResponse dr = { 138 .hr.reply = j, 139 .hr.http_status = (unsigned int) response_code, 140 .reserve_sig = &pch->reserve_sig 141 }; 142 143 pch->job = NULL; 144 switch (response_code) 145 { 146 case 0: 147 dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 148 break; 149 case MHD_HTTP_OK: 150 { 151 struct TALER_Amount total_deposited; 152 struct GNUNET_JSON_Specification spec[] = { 153 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 154 &dr.details.ok.exchange_sig), 155 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 156 &dr.details.ok.exchange_pub), 157 GNUNET_JSON_spec_timestamp ("exchange_timestamp", 158 &dr.details.ok.etime), 159 TALER_JSON_spec_amount ("merge_amount", 160 pch->purse_value_after_fees.currency, 161 &total_deposited), 162 GNUNET_JSON_spec_end () 163 }; 164 165 if (GNUNET_OK != 166 GNUNET_JSON_parse (j, 167 spec, 168 NULL, NULL)) 169 { 170 GNUNET_break_op (0); 171 dr.hr.http_status = 0; 172 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 173 break; 174 } 175 if (GNUNET_OK != 176 TALER_EXCHANGE_test_signing_key (pch->keys, 177 &dr.details.ok.exchange_pub)) 178 { 179 GNUNET_break_op (0); 180 dr.hr.http_status = 0; 181 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID; 182 break; 183 } 184 if (GNUNET_OK != 185 TALER_exchange_online_purse_merged_verify ( 186 dr.details.ok.etime, 187 pch->purse_expiration, 188 &pch->purse_value_after_fees, 189 &pch->purse_pub, 190 &pch->h_contract_terms, 191 &pch->reserve_pub, 192 pch->provider_url, 193 &dr.details.ok.exchange_pub, 194 &dr.details.ok.exchange_sig)) 195 { 196 GNUNET_break_op (0); 197 dr.hr.http_status = 0; 198 dr.hr.ec = TALER_EC_EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID; 199 break; 200 } 201 } 202 break; 203 case MHD_HTTP_BAD_REQUEST: 204 /* This should never happen, either us or the exchange is buggy 205 (or API version conflict); just pass JSON reply to the application */ 206 dr.hr.ec = TALER_JSON_get_error_code (j); 207 dr.hr.hint = TALER_JSON_get_error_hint (j); 208 break; 209 case MHD_HTTP_PAYMENT_REQUIRED: 210 /* purse was not (yet) full */ 211 dr.hr.ec = TALER_JSON_get_error_code (j); 212 dr.hr.hint = TALER_JSON_get_error_hint (j); 213 break; 214 case MHD_HTTP_FORBIDDEN: 215 dr.hr.ec = TALER_JSON_get_error_code (j); 216 dr.hr.hint = TALER_JSON_get_error_hint (j); 217 /* Nothing really to verify, exchange says one of the signatures is 218 invalid; as we checked them, this should never happen, we 219 should pass the JSON reply to the application */ 220 break; 221 case MHD_HTTP_NOT_FOUND: 222 dr.hr.ec = TALER_JSON_get_error_code (j); 223 dr.hr.hint = TALER_JSON_get_error_hint (j); 224 /* Nothing really to verify, this should never 225 happen, we should pass the JSON reply to the application */ 226 break; 227 case MHD_HTTP_CONFLICT: 228 { 229 struct TALER_PurseMergePublicKeyP merge_pub; 230 231 GNUNET_CRYPTO_eddsa_key_get_public (&pch->merge_priv.eddsa_priv, 232 &merge_pub.eddsa_pub); 233 if (GNUNET_OK != 234 TALER_EXCHANGE_check_purse_merge_conflict_ ( 235 &pch->merge_sig, 236 &merge_pub, 237 &pch->purse_pub, 238 pch->provider_url, 239 j)) 240 { 241 GNUNET_break_op (0); 242 dr.hr.http_status = 0; 243 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 244 break; 245 } 246 break; 247 } 248 break; 249 case MHD_HTTP_GONE: 250 /* could happen if denomination was revoked */ 251 /* Note: one might want to check /keys for revocation 252 signature here, alas tricky in case our /keys 253 is outdated => left to clients */ 254 dr.hr.ec = TALER_JSON_get_error_code (j); 255 dr.hr.hint = TALER_JSON_get_error_hint (j); 256 break; 257 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 258 { 259 struct GNUNET_JSON_Specification spec[] = { 260 GNUNET_JSON_spec_uint64 ( 261 "requirement_row", 262 &dr.details.unavailable_for_legal_reasons.requirement_row), 263 GNUNET_JSON_spec_end () 264 }; 265 266 if (GNUNET_OK != 267 GNUNET_JSON_parse (j, 268 spec, 269 NULL, NULL)) 270 { 271 GNUNET_break_op (0); 272 dr.hr.http_status = 0; 273 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 274 break; 275 } 276 } 277 break; 278 case MHD_HTTP_INTERNAL_SERVER_ERROR: 279 dr.hr.ec = TALER_JSON_get_error_code (j); 280 dr.hr.hint = TALER_JSON_get_error_hint (j); 281 /* Server had an internal issue; we should retry, but this API 282 leaves this to the application */ 283 break; 284 default: 285 /* unexpected response code */ 286 dr.hr.ec = TALER_JSON_get_error_code (j); 287 dr.hr.hint = TALER_JSON_get_error_hint (j); 288 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 289 "Unexpected response code %u/%d for exchange deposit\n", 290 (unsigned int) response_code, 291 dr.hr.ec); 292 GNUNET_break_op (0); 293 break; 294 } 295 pch->cb (pch->cb_cls, 296 &dr); 297 TALER_EXCHANGE_account_merge_cancel (pch); 298 } 299 300 301 struct TALER_EXCHANGE_AccountMergeHandle * 302 TALER_EXCHANGE_account_merge ( 303 struct GNUNET_CURL_Context *ctx, 304 const char *url, 305 struct TALER_EXCHANGE_Keys *keys, 306 const char *reserve_exchange_url, 307 const struct TALER_ReservePrivateKeyP *reserve_priv, 308 const struct TALER_PurseContractPublicKeyP *purse_pub, 309 const struct TALER_PurseMergePrivateKeyP *merge_priv, 310 const struct TALER_PrivateContractHashP *h_contract_terms, 311 uint8_t min_age, 312 const struct TALER_Amount *purse_value_after_fees, 313 struct GNUNET_TIME_Timestamp purse_expiration, 314 struct GNUNET_TIME_Timestamp merge_timestamp, 315 TALER_EXCHANGE_AccountMergeCallback cb, 316 void *cb_cls) 317 { 318 struct TALER_EXCHANGE_AccountMergeHandle *pch; 319 json_t *merge_obj; 320 CURL *eh; 321 char arg_str[sizeof (pch->purse_pub) * 2 + 32]; 322 struct TALER_NormalizedPayto reserve_url; 323 324 pch = GNUNET_new (struct TALER_EXCHANGE_AccountMergeHandle); 325 pch->merge_priv = *merge_priv; 326 pch->cb = cb; 327 pch->cb_cls = cb_cls; 328 pch->purse_pub = *purse_pub; 329 pch->h_contract_terms = *h_contract_terms; 330 pch->purse_expiration = purse_expiration; 331 pch->purse_value_after_fees = *purse_value_after_fees; 332 if (NULL == reserve_exchange_url) 333 pch->provider_url = GNUNET_strdup (url); 334 else 335 pch->provider_url = GNUNET_strdup (reserve_exchange_url); 336 GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, 337 &pch->reserve_pub.eddsa_pub); 338 339 { 340 char pub_str[sizeof (*purse_pub) * 2]; 341 char *end; 342 343 end = GNUNET_STRINGS_data_to_string ( 344 purse_pub, 345 sizeof (*purse_pub), 346 pub_str, 347 sizeof (pub_str)); 348 *end = '\0'; 349 GNUNET_snprintf (arg_str, 350 sizeof (arg_str), 351 "purses/%s/merge", 352 pub_str); 353 } 354 reserve_url = TALER_reserve_make_payto (pch->provider_url, 355 &pch->reserve_pub); 356 if (NULL == reserve_url.normalized_payto) 357 { 358 GNUNET_break (0); 359 GNUNET_free (pch->provider_url); 360 GNUNET_free (pch); 361 return NULL; 362 } 363 pch->url = TALER_url_join (url, 364 arg_str, 365 NULL); 366 if (NULL == pch->url) 367 { 368 GNUNET_break (0); 369 GNUNET_free (reserve_url.normalized_payto); 370 GNUNET_free (pch->provider_url); 371 GNUNET_free (pch); 372 return NULL; 373 } 374 TALER_wallet_purse_merge_sign (reserve_url, 375 merge_timestamp, 376 purse_pub, 377 merge_priv, 378 &pch->merge_sig); 379 { 380 struct TALER_Amount zero_purse_fee; 381 382 GNUNET_assert (GNUNET_OK == 383 TALER_amount_set_zero (purse_value_after_fees->currency, 384 &zero_purse_fee)); 385 TALER_wallet_account_merge_sign (merge_timestamp, 386 purse_pub, 387 purse_expiration, 388 h_contract_terms, 389 purse_value_after_fees, 390 &zero_purse_fee, 391 min_age, 392 TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, 393 reserve_priv, 394 &pch->reserve_sig); 395 } 396 merge_obj = GNUNET_JSON_PACK ( 397 TALER_JSON_pack_normalized_payto ("payto_uri", 398 reserve_url), 399 GNUNET_JSON_pack_data_auto ("merge_sig", 400 &pch->merge_sig), 401 GNUNET_JSON_pack_data_auto ("reserve_sig", 402 &pch->reserve_sig), 403 GNUNET_JSON_pack_timestamp ("merge_timestamp", 404 merge_timestamp)); 405 GNUNET_assert (NULL != merge_obj); 406 GNUNET_free (reserve_url.normalized_payto); 407 eh = TALER_EXCHANGE_curl_easy_get_ (pch->url); 408 if ( (NULL == eh) || 409 (GNUNET_OK != 410 TALER_curl_easy_post (&pch->ctx, 411 eh, 412 merge_obj)) ) 413 { 414 GNUNET_break (0); 415 if (NULL != eh) 416 curl_easy_cleanup (eh); 417 json_decref (merge_obj); 418 GNUNET_free (pch->provider_url); 419 GNUNET_free (pch->url); 420 GNUNET_free (pch); 421 return NULL; 422 } 423 json_decref (merge_obj); 424 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 425 "URL for purse merge: `%s'\n", 426 pch->url); 427 pch->keys = TALER_EXCHANGE_keys_incref (keys); 428 pch->job = GNUNET_CURL_job_add2 (ctx, 429 eh, 430 pch->ctx.headers, 431 &handle_purse_merge_finished, 432 pch); 433 return pch; 434 } 435 436 437 void 438 TALER_EXCHANGE_account_merge_cancel ( 439 struct TALER_EXCHANGE_AccountMergeHandle *pch) 440 { 441 if (NULL != pch->job) 442 { 443 GNUNET_CURL_job_cancel (pch->job); 444 pch->job = NULL; 445 } 446 GNUNET_free (pch->url); 447 GNUNET_free (pch->provider_url); 448 TALER_curl_easy_post_finished (&pch->ctx); 449 TALER_EXCHANGE_keys_decref (pch->keys); 450 GNUNET_free (pch); 451 } 452 453 454 /* end of exchange_api_purse_merge.c */