taler-exchange-httpd_reveal-withdraw.c (21429B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2023,2025 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero 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 Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file taler-exchange-httpd_reveal-withdraw.c 18 * @brief Handle /reveal-withdraw requests 19 * @author Özgür Kesim 20 */ 21 #include "taler/platform.h" 22 #include <gnunet/gnunet_common.h> 23 #include <gnunet/gnunet_util_lib.h> 24 #include <jansson.h> 25 #include <microhttpd.h> 26 #include "taler-exchange-httpd_metrics.h" 27 #include "taler/taler_error_codes.h" 28 #include "taler/taler_exchangedb_plugin.h" 29 #include "taler/taler_mhd_lib.h" 30 #include "taler-exchange-httpd_mhd.h" 31 #include "taler-exchange-httpd_reveal-withdraw.h" 32 #include "taler-exchange-httpd_responses.h" 33 #include "taler-exchange-httpd_keys.h" 34 35 /** 36 * State for an /reveal-withdraw operation. 37 */ 38 struct WithdrawRevealContext 39 { 40 41 /** 42 * Commitment for the withdraw operation, previously called by the 43 * client. 44 */ 45 struct TALER_HashBlindedPlanchetsP planchets_h; 46 47 /** 48 * TALER_CNC_KAPPA-1 secrets for disclosed coin batches. 49 */ 50 struct TALER_RevealWithdrawMasterSeedsP disclosed_batch_seeds; 51 52 /** 53 * The data from the original withdraw. Will be retrieved from 54 * the DB via @a wch. 55 */ 56 struct TALER_EXCHANGEDB_Withdraw withdraw; 57 }; 58 59 60 /** 61 * Parse the json body of an '/reveal-withdraw' request. It extracts 62 * the denomination hashes, blinded coins and disclosed coins and allocates 63 * memory for those. 64 * 65 * @param connection The MHD connection to handle 66 * @param j_disclosed_batch_seeds The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from 67 * @param[out] actx The context of the operation, only partially built at call time 68 * @param[out] mhd_ret The result if a reply is queued for MHD 69 * @return true on success, false on failure, with a reply already queued for MHD. 70 */ 71 static enum GNUNET_GenericReturnValue 72 parse_withdraw_reveal_json ( 73 struct MHD_Connection *connection, 74 const json_t *j_disclosed_batch_seeds, 75 struct WithdrawRevealContext *actx, 76 MHD_RESULT *mhd_ret) 77 { 78 size_t num_entries; 79 const char *error; 80 struct GNUNET_JSON_Specification tuple[] = { 81 GNUNET_JSON_spec_fixed (NULL, 82 &actx->disclosed_batch_seeds.tuple[0], 83 sizeof(actx->disclosed_batch_seeds.tuple[0])), 84 GNUNET_JSON_spec_fixed (NULL, 85 &actx->disclosed_batch_seeds.tuple[1], 86 sizeof(actx->disclosed_batch_seeds.tuple[1])), 87 GNUNET_JSON_spec_end () 88 }; 89 struct GNUNET_JSON_Specification spec[] = { 90 TALER_JSON_spec_tuple_of (NULL, 91 tuple), 92 GNUNET_JSON_spec_end () 93 }; 94 95 /** 96 * Note that above, in tuple[], we have hard-wired 97 * the size of TALER_CNC_KAPPA. 98 * Let's make sure we keep this in sync. 99 */ 100 _Static_assert ((TALER_CNC_KAPPA - 1) == 2); 101 102 num_entries = json_array_size (j_disclosed_batch_seeds); /* 0, if not an array */ 103 if (! json_is_array (j_disclosed_batch_seeds)) 104 error = "disclosed_batch_seeds must be an array"; 105 else if (num_entries == 0) 106 error = "disclosed_batch_seeds must not be empty"; 107 else if (num_entries != TALER_CNC_KAPPA - 1) 108 error = 109 "disclosed_batch_seeds must be an array of size " 110 TALER_CNC_KAPPA_MINUS_ONE_STR; 111 else 112 error = NULL; 113 114 if ( (NULL != error) || 115 (GNUNET_OK != 116 GNUNET_JSON_parse (j_disclosed_batch_seeds, 117 spec, 118 &error, 119 NULL)) ) 120 { 121 GNUNET_break_op (0); 122 *mhd_ret = TALER_MHD_reply_with_ec (connection, 123 TALER_EC_GENERIC_PARAMETER_MALFORMED, 124 error); 125 return GNUNET_SYSERR; 126 } 127 128 return GNUNET_OK; 129 } 130 131 132 /** 133 * Check if the request belongs to an existing withdraw request. 134 * If so, sets the withdraw object with the request data. 135 * Otherwise, it queues an appropriate MHD response. 136 * 137 * @param connection The HTTP connection to the client 138 * @param planchets_h Original commitment value sent with the withdraw request 139 * @param[out] withdraw Data from the original withdraw request 140 * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. 141 * @return #GNUNET_OK if the withdraw request has been found, 142 * #GNUNET_SYSERR if we did not find the request in the DB 143 */ 144 static enum GNUNET_GenericReturnValue 145 find_original_withdraw ( 146 struct MHD_Connection *connection, 147 const struct TALER_HashBlindedPlanchetsP *planchets_h, 148 struct TALER_EXCHANGEDB_Withdraw *withdraw, 149 MHD_RESULT *result) 150 { 151 enum GNUNET_DB_QueryStatus qs; 152 153 for (unsigned int try = 0; try < 3; try++) 154 { 155 qs = TEH_plugin->get_withdraw (TEH_plugin->cls, 156 planchets_h, 157 withdraw); 158 switch (qs) 159 { 160 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 161 return GNUNET_OK; /* Only happy case */ 162 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 163 *result = TALER_MHD_reply_with_error (connection, 164 MHD_HTTP_NOT_FOUND, 165 TALER_EC_EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN, 166 NULL); 167 return GNUNET_SYSERR; 168 case GNUNET_DB_STATUS_HARD_ERROR: 169 *result = TALER_MHD_reply_with_ec (connection, 170 TALER_EC_GENERIC_DB_FETCH_FAILED, 171 "get_withdraw"); 172 return GNUNET_SYSERR; 173 case GNUNET_DB_STATUS_SOFT_ERROR: 174 break; /* try again */ 175 default: 176 GNUNET_break (0); 177 *result = TALER_MHD_reply_with_ec (connection, 178 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 179 NULL); 180 return GNUNET_SYSERR; 181 } 182 } 183 /* after unsuccessful retries*/ 184 GNUNET_break (0); 185 *result = TALER_MHD_reply_with_ec (connection, 186 TALER_EC_GENERIC_DB_FETCH_FAILED, 187 "get_withdraw"); 188 return GNUNET_SYSERR; 189 } 190 191 192 /** 193 * @brief Derives an age-restricted planchet from a given secret and calculates the hash 194 * 195 * @param connection Connection to the client 196 * @param denom_key The denomination key 197 * @param secret The secret to a planchet 198 * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL 199 * @param nonce The derived nonce needed for CS denomination 200 * @param max_age The maximum age allowed 201 * @param[out] bch Hashcode to write 202 * @param[out] result On error, a HTTP-response will be queued and result set accordingly 203 * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise, with an error message 204 * written to the client and @e result set. 205 */ 206 static enum GNUNET_GenericReturnValue 207 calculate_blinded_hash ( 208 struct MHD_Connection *connection, 209 struct TEH_DenominationKey *denom_key, 210 const struct TALER_PlanchetMasterSecretP *secret, 211 const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub, 212 union GNUNET_CRYPTO_BlindSessionNonce *nonce, 213 uint8_t max_age, 214 struct TALER_BlindedCoinHashP *bch, 215 MHD_RESULT *result) 216 { 217 enum GNUNET_GenericReturnValue ret; 218 struct TALER_AgeCommitmentHashP ach; 219 220 /* calculate age commitment hash */ 221 { 222 struct TALER_AgeCommitmentProof acp; 223 224 TALER_age_restriction_from_secret (secret, 225 &denom_key->denom_pub.age_mask, 226 max_age, 227 &acp); 228 TALER_age_commitment_hash (&acp.commitment, 229 &ach); 230 TALER_age_commitment_proof_free (&acp); 231 } 232 233 /* Next: calculate planchet */ 234 { 235 struct TALER_CoinPubHashP c_hash; 236 struct TALER_PlanchetDetail detail = {0}; 237 struct TALER_CoinSpendPrivateKeyP coin_priv; 238 union GNUNET_CRYPTO_BlindingSecretP bks; 239 struct GNUNET_CRYPTO_BlindingInputValues bi = { 240 .cipher = denom_key->denom_pub.bsign_pub_key->cipher 241 }; 242 struct TALER_ExchangeBlindingValues alg_values = { 243 .blinding_inputs = &bi 244 }; 245 246 if (GNUNET_CRYPTO_BSA_CS == bi.cipher) 247 { 248 GNUNET_assert (NULL != r_pub); 249 GNUNET_assert (NULL != nonce); 250 bi.details.cs_values = *r_pub; 251 } 252 TALER_planchet_blinding_secret_create (secret, 253 &alg_values, 254 &bks); 255 TALER_planchet_setup_coin_priv (secret, 256 &alg_values, 257 &coin_priv); 258 ret = TALER_planchet_prepare (&denom_key->denom_pub, 259 &alg_values, 260 &bks, 261 nonce, 262 &coin_priv, 263 &ach, 264 &c_hash, 265 &detail); 266 if (GNUNET_OK != ret) 267 { 268 GNUNET_break (0); 269 *result = TALER_MHD_REPLY_JSON_PACK (connection, 270 MHD_HTTP_INTERNAL_SERVER_ERROR, 271 GNUNET_JSON_pack_string ( 272 "details", 273 "failed to prepare planchet from base key")); 274 return ret; 275 } 276 277 TALER_coin_ev_hash (&detail.blinded_planchet, 278 &denom_key->h_denom_pub, 279 bch); 280 TALER_blinded_planchet_free (&detail.blinded_planchet); 281 } 282 283 return ret; 284 } 285 286 287 /** 288 * @brief Checks the validity of the disclosed coins as follows: 289 * - Derives and calculates the disclosed coins' 290 * - public keys, 291 * - nonces (if applicable), 292 * - age commitments, 293 * - blindings 294 * - blinded hashes 295 * - Computes planchets_h with those calculated and the undisclosed hashes 296 * - Compares planchets_h with the value from the original commitment 297 * - Verifies that all public keys in indices larger than the age group 298 * corresponding to max_age are derived from the constant public key. 299 * 300 * The derivation of the blindings, (potential) nonces and age-commitment from 301 * a coin's private keys is defined in 302 * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw 303 * 304 * @param con HTTP-connection to the client 305 * @param wd Original withdraw request 306 * @param disclosed_batch_seeds The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many 307 * @param[out] result On error, a HTTP-response will be queued and result set accordingly 308 * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise 309 */ 310 static enum GNUNET_GenericReturnValue 311 verify_commitment_and_max_age ( 312 struct MHD_Connection *con, 313 const struct TALER_EXCHANGEDB_Withdraw *wd, 314 const struct TALER_RevealWithdrawMasterSeedsP *disclosed_batch_seeds, 315 MHD_RESULT *result) 316 { 317 enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; 318 struct GNUNET_HashContext *hash_context; 319 struct TEH_KeyStateHandle *keys; 320 struct TEH_DenominationKey *denom_keys[wd->num_coins]; 321 struct TALER_Amount total_amount; 322 struct TALER_Amount total_fee; 323 struct TALER_AgeMask mask; 324 struct TALER_PlanchetMasterSecretP secrets[ 325 TALER_CNC_KAPPA - 1][wd->num_coins]; 326 bool is_cs[wd->num_coins]; 327 size_t cs_count = 0; 328 uint8_t secrets_idx = 0; /* first index into secrets */ 329 330 GNUNET_assert (wd->noreveal_index < TALER_CNC_KAPPA); 331 332 GNUNET_assert (GNUNET_OK == 333 TALER_amount_set_zero (TEH_currency, 334 &total_amount)); 335 GNUNET_assert (GNUNET_OK == 336 TALER_amount_set_zero (TEH_currency, 337 &total_fee)); 338 339 memset (denom_keys, 340 0, 341 sizeof(denom_keys)); 342 memset (is_cs, 343 0, 344 sizeof(is_cs)); 345 346 /* We need the current keys in memory for the meta-data of the denominations */ 347 keys = TEH_keys_get_state (); 348 if (NULL == keys) 349 { 350 *result = TALER_MHD_reply_with_ec (con, 351 TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, 352 NULL); 353 return GNUNET_SYSERR; 354 } 355 356 /* Find the denomination keys */ 357 for (size_t i = 0; i < wd->num_coins; i++) 358 { 359 denom_keys[i] = 360 TEH_keys_denomination_by_serial_from_state ( 361 keys, 362 wd->denom_serials[i]); 363 if (NULL == denom_keys[i]) 364 { 365 GNUNET_break_op (0); 366 *result = TALER_MHD_reply_with_ec (con, 367 TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, 368 NULL); 369 return GNUNET_SYSERR; 370 } 371 372 /* Accumulate amount and fees */ 373 GNUNET_assert (0 <= TALER_amount_add (&total_amount, 374 &total_amount, 375 &denom_keys[i]->meta.value)); 376 GNUNET_assert (0 <= TALER_amount_add (&total_fee, 377 &total_fee, 378 &denom_keys[i]->meta.fees.withdraw)); 379 380 if (i == 0) 381 mask = denom_keys[i]->meta.age_mask; 382 GNUNET_assert (mask.bits == denom_keys[i]->meta.age_mask.bits); 383 384 if (GNUNET_CRYPTO_BSA_CS == 385 denom_keys[i]->denom_pub.bsign_pub_key->cipher) 386 { 387 is_cs[i] = true; 388 cs_count++; 389 } 390 } 391 392 hash_context = GNUNET_CRYPTO_hash_context_start (); 393 394 { 395 uint32_t cs_indices[cs_count]; 396 union GNUNET_CRYPTO_BlindSessionNonce nonces[cs_count]; 397 size_t cs_idx = 0; /* [0...cs_count) */ 398 399 for (size_t i = 0; i < wd->num_coins; i++) 400 if (is_cs[i]) 401 cs_indices[cs_idx++] = i; 402 403 TALER_cs_derive_only_cs_blind_nonces_from_seed (&wd->blinding_seed, 404 false, /* not for melt */ 405 cs_count, 406 cs_indices, 407 nonces); 408 409 for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++) 410 { 411 if (gamma == wd->noreveal_index) 412 { 413 /** 414 * For the disclosed index, all we have to do is to accumulate the hash 415 * of the selected coins 416 */ 417 GNUNET_CRYPTO_hash_context_read ( 418 hash_context, 419 &wd->selected_h, 420 sizeof(wd->selected_h)); 421 } 422 else 423 { 424 /** 425 * For the non-disclosed index, we have to generate the planchets in 426 * this batch and calculate their hash 427 */ 428 struct GNUNET_HashContext *batch_ctx; 429 struct TALER_BlindedCoinHashP batch_h; 430 cs_idx = 0; 431 432 batch_ctx = GNUNET_CRYPTO_hash_context_start (); 433 GNUNET_assert (NULL != batch_ctx); 434 435 /* Expand the secrets for a disclosed batch */ 436 GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1)); 437 TALER_withdraw_expand_secrets ( 438 wd->num_coins, 439 &disclosed_batch_seeds->tuple[secrets_idx], 440 secrets[secrets_idx]); 441 442 /** 443 * Now individually create each coin in this batch and calculate 444 * its hash, and accumulate the hash of the batch with it 445 */ 446 for (size_t coin_idx = 0; coin_idx < wd->num_coins; coin_idx++) 447 { 448 struct TALER_BlindedCoinHashP bch; 449 struct GNUNET_CRYPTO_CSPublicRPairP *rp; 450 union GNUNET_CRYPTO_BlindSessionNonce *np; 451 452 if (is_cs[coin_idx]) 453 { 454 GNUNET_assert (cs_idx < cs_count); 455 np = &nonces[cs_idx]; 456 rp = &wd->cs_r_values[cs_idx]; 457 cs_idx++; 458 } 459 else 460 { 461 np = NULL; 462 rp = NULL; 463 } 464 ret = calculate_blinded_hash (con, 465 denom_keys[coin_idx], 466 &secrets[secrets_idx][coin_idx], 467 rp, 468 np, 469 wd->max_age, 470 &bch, 471 result); 472 if (GNUNET_OK != ret) 473 { 474 GNUNET_CRYPTO_hash_context_abort (hash_context); 475 return GNUNET_SYSERR; 476 } 477 /** 478 * Continue the running hash of all coin hashes in the batch 479 * with the calculated hash-value of the current, disclosed coin 480 */ 481 GNUNET_CRYPTO_hash_context_read (batch_ctx, 482 &bch, 483 sizeof(bch)); 484 } 485 /** 486 * Finalize the hash of this batch and add it to 487 * the total hash 488 */ 489 GNUNET_CRYPTO_hash_context_finish ( 490 batch_ctx, 491 &batch_h.hash); 492 GNUNET_CRYPTO_hash_context_read ( 493 hash_context, 494 &batch_h, 495 sizeof(batch_h)); 496 497 secrets_idx++; 498 } 499 } 500 } 501 502 /* Finally, compare the calculated hash with the original wd */ 503 { 504 struct TALER_HashBlindedPlanchetsP planchets_h; 505 506 GNUNET_CRYPTO_hash_context_finish ( 507 hash_context, 508 &planchets_h.hash); 509 510 if (0 != GNUNET_CRYPTO_hash_cmp ( 511 &wd->planchets_h.hash, 512 &planchets_h.hash)) 513 { 514 GNUNET_break_op (0); 515 *result = TALER_MHD_reply_with_ec (con, 516 TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH, 517 NULL); 518 return GNUNET_SYSERR; 519 } 520 521 } 522 return GNUNET_OK; 523 } 524 525 526 /** 527 * @brief Send a response for "/reveal-withdraw" 528 * 529 * @param connection The http connection to the client to send the response to 530 * @param commitment The data from the commitment with signatures 531 * @return a MHD result code 532 */ 533 static MHD_RESULT 534 reply_withdraw_reveal_success ( 535 struct MHD_Connection *connection, 536 const struct TALER_EXCHANGEDB_Withdraw *commitment) 537 { 538 json_t *list = json_array (); 539 540 GNUNET_assert (NULL != list); 541 for (size_t i = 0; i < commitment->num_coins; i++) 542 { 543 json_t *obj = GNUNET_JSON_PACK ( 544 TALER_JSON_pack_blinded_denom_sig (NULL, 545 &commitment->denom_sigs[i])); 546 GNUNET_assert (0 == 547 json_array_append_new (list, 548 obj)); 549 } 550 551 return TALER_MHD_REPLY_JSON_PACK ( 552 connection, 553 MHD_HTTP_OK, 554 GNUNET_JSON_pack_array_steal ("ev_sigs", 555 list)); 556 } 557 558 559 MHD_RESULT 560 TEH_handler_reveal_withdraw ( 561 struct TEH_RequestContext *rc, 562 const json_t *root, 563 const char *const args[0]) 564 { 565 MHD_RESULT result = MHD_NO; 566 enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; 567 struct WithdrawRevealContext actx = {0}; 568 const json_t *j_disclosed_batch_seeds; 569 struct GNUNET_JSON_Specification spec[] = { 570 GNUNET_JSON_spec_fixed_auto ("planchets_h", 571 &actx.planchets_h), 572 GNUNET_JSON_spec_array_const ("disclosed_batch_seeds", 573 &j_disclosed_batch_seeds), 574 GNUNET_JSON_spec_end () 575 }; 576 577 (void) args; 578 /* Parse JSON body*/ 579 ret = TALER_MHD_parse_json_data (rc->connection, 580 root, 581 spec); 582 if (GNUNET_OK != ret) 583 { 584 GNUNET_break_op (0); 585 return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; 586 } 587 588 do { 589 /* Extract denominations, blinded and disclosed coins */ 590 if (GNUNET_OK != 591 parse_withdraw_reveal_json ( 592 rc->connection, 593 j_disclosed_batch_seeds, 594 &actx, 595 &result)) 596 break; 597 598 /* Find original commitment */ 599 if (GNUNET_OK != 600 find_original_withdraw ( 601 rc->connection, 602 &actx.planchets_h, 603 &actx.withdraw, 604 &result)) 605 break; 606 607 /* Verify the computed planchets_h equals the committed one and that coins 608 * have a maximum age group corresponding max_age (age-mask dependent) */ 609 if (GNUNET_OK != 610 verify_commitment_and_max_age ( 611 rc->connection, 612 &actx.withdraw, 613 &actx.disclosed_batch_seeds, 614 &result)) 615 break; 616 617 /* Finally, return the signatures */ 618 result = reply_withdraw_reveal_success (rc->connection, 619 &actx.withdraw); 620 621 } while (0); 622 623 GNUNET_JSON_parse_free (spec); 624 if (NULL != actx.withdraw.denom_sigs) 625 { 626 for (size_t i = 0; i<actx.withdraw.num_coins; i++) 627 TALER_blinded_denom_sig_free (&actx.withdraw.denom_sigs[i]); 628 GNUNET_free (actx.withdraw.denom_sigs); 629 } 630 GNUNET_free (actx.withdraw.cs_r_values); 631 GNUNET_free (actx.withdraw.denom_pub_hashes); 632 GNUNET_free (actx.withdraw.denom_serials); 633 return result; 634 } 635 636 637 /* end of taler-exchange-httpd_reveal_withdraw.c */