exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 41b10090a3443f77f6f6a76c6a04132229d398c8
parent ccbfca3f47e03c4876d617788c0987c050785301
Author: Özgür Kesim <oec@codeblau.de>
Date:   Fri, 11 Apr 2025 23:45:46 +0200

[withdraw+blinding-prepare] correction of Clause-Schnorr handling

- /blinding-prepare now takes a single seed from which all nonces are calculated
- /withdraw requires a blinding_seed when CS denominations are involved
  - that value is also part of the TALER_WithdrawCommitmentPS struct
- the exchange pre-calculates the public R-values and stores them along
  with the blinding_seed in the withdraw table
- the do_withdraw() SQL logic now properly detects nonce-reuse and conflicts
- conflict and nonce-reuse tests have been added to src/exchangedb/test_exchangedb.c
- additional query/result helpers for R-values were added to lib/pq
- the C-API has been adapted to the new /blinding-prepare and /withdraw endpoints
- tests in src/testing have been adapted to the new C-API

all tests pass.
(except for auditor, for which tests are disabled for now, as wallet-core is not ready)

Diffstat:
Msrc/auditor/taler-helper-auditor-coins.c | 3+++
Msrc/auditor/taler-helper-auditor-reserves.c | 3+++
Msrc/exchange/taler-exchange-httpd_blinding-prepare.c | 141+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/exchange/taler-exchange-httpd_keys.c | 36++++++++++++++++++++++++++++++++++++
Msrc/exchange/taler-exchange-httpd_keys.h | 18++++++++++++++++++
Msrc/exchange/taler-exchange-httpd_melt_v26.c | 15+++++----------
Msrc/exchange/taler-exchange-httpd_reserves_history.c | 15+++++++++++++++
Msrc/exchange/taler-exchange-httpd_reveal-withdraw.c | 230++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/exchange/taler-exchange-httpd_withdraw.c | 332+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/exchangedb/0009-withdraw.sql | 23+++++++++++++++++++++--
Msrc/exchangedb/exchange_do_withdraw.sql | 115++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/exchangedb/pg_do_withdraw.c | 21++++++++++++++++++---
Msrc/exchangedb/pg_do_withdraw.h | 22++++++++++++----------
Msrc/exchangedb/pg_get_reserve_history.c | 22+++++++++++++++++++++-
Msrc/exchangedb/pg_get_withdraw.c | 53++++++++++++++++++++++++++++++++++++-----------------
Msrc/exchangedb/pg_insert_records_by_table.c | 16++++++++++++++--
Msrc/exchangedb/pg_lookup_records_by_table.c | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/exchangedb/pg_select_withdrawals_above_serial_id.c | 8++++++++
Msrc/exchangedb/test_exchangedb.c | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/include/taler_crypto_lib.h | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/include/taler_exchange_service.h | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/include/taler_exchangedb_plugin.h | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/include/taler_pq_lib.h | 36++++++++++++++++++++++++++++++++++--
Msrc/include/taler_testing_lib.h | 9+++++++--
Msrc/lib/exchange_api_blinding_prepare.c | 61+++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/lib/exchange_api_reserves_history.c | 10++++++++++
Msrc/lib/exchange_api_withdraw.c | 306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/pq/pq_common.h | 3+--
Msrc/pq/pq_query_helper.c | 33+++++++++++++++++++++++++++++++++
Msrc/pq/pq_result_helper.c | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/testing_api_cmd_age_withdraw.c | 66++++++++++++++++++++----------------------------------------------
Msrc/testing/testing_api_cmd_batch_withdraw.c | 2+-
Msrc/testing/testing_api_cmd_withdraw.c | 22+++++++++++++++-------
Msrc/util/crypto.c | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/util/test_crypto.c | 1+
Msrc/util/test_helper_cs.c | 1+
Msrc/util/wallet_signatures.c | 15+++++++++++++++
37 files changed, 1627 insertions(+), 553 deletions(-)

diff --git a/src/auditor/taler-helper-auditor-coins.c b/src/auditor/taler-helper-auditor-coins.c @@ -982,6 +982,7 @@ cleanup_denomination (void *cls, * @param h_blind_evs array @e num_evs of blinded hashes of the coin's public keys * @param denom_serials array @e num_evs of serial ID's of denominations in our DB * @param h_planchets running hash over all hashes of blinded planchets in the original withdraw request + * @param blinding_seed the master seed for CS denominations that was provided during withdraw; might be NULL * @param age_proof_required true if the withdraw request required an age proof. * @param max_age if @e age_proof_required is true, the maximum age that was set on the coins. * @param noreveal_index if @e age_proof_required is true, the index that was returned by the exchange for the reveal phase. @@ -998,6 +999,7 @@ withdraw_cb (void *cls, const struct TALER_BlindedCoinHashP *h_blind_evs, const uint64_t *denom_serials, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, bool age_proof_required, uint8_t max_age, uint8_t noreveal_index, @@ -1012,6 +1014,7 @@ withdraw_cb (void *cls, could avoid fetching from the database with a custom function. */ (void) h_blind_evs; (void) h_planchets; + (void) blinding_seed; (void) reserve_pub; (void) reserve_sig; (void) execution_date; diff --git a/src/auditor/taler-helper-auditor-reserves.c b/src/auditor/taler-helper-auditor-reserves.c @@ -516,6 +516,7 @@ handle_reserve_in ( * @param h_blind_evs array @e num_evs of blinded hashes of the coin's public keys * @param denom_serials array @e num_evs of serial ID's of denominations in our DB * @param h_planchets running hash over all hashes of blinded planchets in the original withdraw request + * @param blinding_seed the master seed used for CS denominations; might be NULL * @param age_proof_required true if the withdraw request required an age proof. * @param max_age if @e age_proof_required is true, the maximum age that was set on the coins. * @param noreveal_index if @e age_proof_required is true, the index that was returned by the exchange for the reveal phase. @@ -533,6 +534,7 @@ handle_withdrawals ( const struct TALER_BlindedCoinHashP *h_blind_evs, const uint64_t *denom_serials, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, bool age_proof_required, uint8_t max_age, uint8_t noreveal_index, @@ -645,6 +647,7 @@ handle_withdrawals ( &auditor_amount, &auditor_fee, h_planchets, + blinding_seed, age_proof_required ? &issue->age_mask : NULL, diff --git a/src/exchange/taler-exchange-httpd_blinding-prepare.c b/src/exchange/taler-exchange-httpd_blinding-prepare.c @@ -35,100 +35,113 @@ TEH_handler_blinding_prepare (struct TEH_RequestContext *rc, const json_t *root, const char *const args[]) { - const json_t *j_nonces; - const json_t *j_denoms_h; + struct TALER_BlindingMasterSeedP blinding_seed; const char *cipher; + const char *operation; + const json_t *j_nks; size_t num; + bool is_melt; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("cipher", &cipher), - GNUNET_JSON_spec_array_const ("nonces", - &j_nonces), - GNUNET_JSON_spec_array_const ("denoms_h", - &j_denoms_h), + GNUNET_JSON_spec_string ("operation", + &operation), + GNUNET_JSON_spec_fixed_auto ("seed", + &blinding_seed), + GNUNET_JSON_spec_array_const ("nks", + &j_nks), GNUNET_JSON_spec_end () }; + enum GNUNET_GenericReturnValue res; (void) args; - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_OK != res) - return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; - } - num = json_array_size (j_nonces); + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + if (0 == strcmp (operation, "melt")) { - const char *error = NULL; - - if (num != json_array_size (j_denoms_h)) - { - GNUNET_break_op (0); - error = "arrays not of equal size"; - } - else if (0 == num || TALER_MAX_FRESH_COINS < num) - { - GNUNET_break_op (0); - error = "invalid number of entries"; - } - else if (0 != strcmp (cipher, "CS")) - { - GNUNET_break_op (0); - error = "wrong cipher"; - } + is_melt = true; + } + else if + (0 == strcmp (operation, "withdraw")) + { + is_melt = false; + } + else + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "operation"); + } - if (NULL != error) - { - TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - error); - } + num = json_array_size (j_nks); + if ((0 == num) || + (TALER_MAX_FRESH_COINS < num)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE, + "nks"); + } + if (0 != strcmp (cipher, "CS")) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + " cipher"); } { - struct GNUNET_CRYPTO_CsSessionNonce nonces[num]; + uint32_t cs_indices[num]; struct TALER_DenominationHashP h_denom_pubs[num]; + struct GNUNET_CRYPTO_CsSessionNonce nonces[num]; - for (size_t idx = 0; idx < num; idx++) + for (size_t i = 0; i < num; i++) { - struct GNUNET_JSON_Specification denom_spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &h_denom_pubs[idx]), - GNUNET_JSON_spec_end () - }; - struct GNUNET_JSON_Specification nonce_spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &nonces[idx]), + enum GNUNET_GenericReturnValue res; + struct GNUNET_JSON_Specification nks_spec[] = { + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &h_denom_pubs[i]), + GNUNET_JSON_spec_uint32 ("coin_offset", + &cs_indices[i]), GNUNET_JSON_spec_end () }; - enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_array (rc->connection, - j_denoms_h, - denom_spec, - idx, - -1); - if (GNUNET_OK != res) - return (GNUNET_SYSERR == res) - ? MHD_NO - : MHD_YES; - res = TALER_MHD_parse_json_array (rc->connection, - j_nonces, - nonce_spec, - idx, + j_nks, + nks_spec, + i, -1); if (GNUNET_OK != res) return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + + if (TALER_MAX_FRESH_COINS < cs_indices[i]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "nks"); + } } + TALER_cs_derive_nonces_from_seed (&blinding_seed, + is_melt, + num, + cs_indices, + nonces); + + { struct TEH_KeyStateHandle *ksh; struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[num]; diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c @@ -280,6 +280,14 @@ struct TEH_KeyStateHandle struct GNUNET_CONTAINER_MultiHashMap *denomkey_map; /** + * Mapping from serial ID's to denomination key issue struct. + * Used to lookup the key by serial ID. + * + * FIXME: We need a 64-bit version of this in GNUNET. + */ + struct GNUNET_CONTAINER_MultiHashMap32 *denomserial_map; + + /** * Map from `struct TALER_ExchangePublicKey` to `struct SigningKey` * entries. Based on the fact that a `struct GNUNET_PeerIdentity` is also * an EdDSA public key. @@ -1763,6 +1771,7 @@ destroy_key_state (struct TEH_KeyStateHandle *ksh, &clear_denomination_cb, ksh); GNUNET_CONTAINER_multihashmap_destroy (ksh->denomkey_map); + GNUNET_CONTAINER_multihashmap32_destroy (ksh->denomserial_map); GNUNET_CONTAINER_multipeermap_iterate (ksh->signkey_map, &clear_signkey_cb, ksh); @@ -1944,6 +1953,17 @@ denomination_info_cb ( &dk->h_denom_pub.hash, dk, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + { + uint32_t serial32 = (uint32_t) dk->meta.serial; + + GNUNET_assert (dk->meta.serial == (uint64_t) serial32); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap32_put (ksh->denomserial_map, + serial32, + dk, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } } @@ -3253,6 +3273,7 @@ build_key_state (struct HelperState *hs, { ksh->helpers = hs; } + ksh->denomserial_map = GNUNET_CONTAINER_multihashmap32_create (1024); ksh->denomkey_map = GNUNET_CONTAINER_multihashmap_create (1024, true); ksh->signkey_map = GNUNET_CONTAINER_multipeermap_create (32, @@ -3492,6 +3513,21 @@ TEH_keys_denomination_by_hash_from_state ( } +struct TEH_DenominationKey * +TEH_keys_denomination_by_serial_from_state ( + const struct TEH_KeyStateHandle *ksh, + uint64_t denom_serial) +{ + struct TEH_DenominationKey *dk; + uint32_t serial32 = (uint32_t) denom_serial; + + GNUNET_assert (denom_serial == (uint64_t) serial32); + dk = GNUNET_CONTAINER_multihashmap32_get (ksh->denomserial_map, + serial32); + return dk; +} + + enum TALER_ErrorCode TEH_keys_denomination_batch_sign ( unsigned int csds_length, diff --git a/src/exchange/taler-exchange-httpd_keys.h b/src/exchange/taler-exchange-httpd_keys.h @@ -283,6 +283,24 @@ TEH_keys_denomination_by_hash_from_state ( struct MHD_Connection *conn, MHD_RESULT *mret); + +/** + * Look up the issue for a denom public key using a given @a ksh. This allows + * requesting multiple denominations with the same @a ksh which thus will + * remain valid until the next call to #TEH_keys_denomination_by_hash() or + * #TEH_keys_get_state() or #TEH_keys_exchange_sign(). + * + * @param ksh key state state to look in + * @param denom_serial serial ID of the denomination in the table + * @return the denomination key issue, + * or NULL if @a denom_serial could not be found + */ +struct TEH_DenominationKey * +TEH_keys_denomination_by_serial_from_state ( + const struct TEH_KeyStateHandle *ksh, + uint64_t denom_serial); + + /** * Information needed to create a blind signature. */ diff --git a/src/exchange/taler-exchange-httpd_melt_v26.c b/src/exchange/taler-exchange-httpd_melt_v26.c @@ -142,15 +142,11 @@ struct MeltContext */ struct TALER_EXCHANGEDB_Refresh_v26 refresh_idem; - /** - * Refresh master secret, if any of the fresh denominations use CS. - */ - struct TALER_RefreshMasterSecretP rms; /** - * True if @e rms is missing. + * True if @e blinding_seed is missing in the request */ - bool no_rms; + bool no_blinding_seed; /** * Set to true if this coin's denomination was revoked and the operation @@ -344,9 +340,9 @@ melt_new_request ( TEH_currency, &mc->request.refresh.amount_with_fee), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("rms", - &mc->request.rms), - &mc->request.no_rms), + GNUNET_JSON_spec_fixed_auto ("blinding_seed", + &mc->request.refresh.blinding_seed), + &mc->request.no_blinding_seed), GNUNET_JSON_spec_array_const ("denoms_h", &j_denoms_h), GNUNET_JSON_spec_array_const ("coin_evs", @@ -1142,7 +1138,6 @@ melt_transaction ( qs = TEH_plugin->do_melt_v26 (TEH_plugin->cls, &mc->request.refresh, - &mc->request.rms, &mc->now, &conflict, &zombie_required, diff --git a/src/exchange/taler-exchange-httpd_reserves_history.c b/src/exchange/taler-exchange-httpd_reserves_history.c @@ -162,6 +162,21 @@ compile_reserve_history ( &withdraw->amount_with_fee) ); + if (! withdraw->no_blinding_seed) + { + if (0!= + json_object_update_new ( + j_entry, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ( + "blinding_seed", + &withdraw->blinding_seed)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } if (withdraw->age_proof_required) { if (0 != diff --git a/src/exchange/taler-exchange-httpd_reveal-withdraw.c b/src/exchange/taler-exchange-httpd_reveal-withdraw.c @@ -53,7 +53,7 @@ struct WithdrawRevealContext * The data from the original withdraw. Will be retrieved from * the DB via @a wch. */ - struct TALER_EXCHANGEDB_Withdraw commitment; + struct TALER_EXCHANGEDB_Withdraw withdraw; }; @@ -129,21 +129,21 @@ parse_withdraw_reveal_json ( /** * Check if the request belongs to an existing withdraw request. - * If so, sets the commitment object with the request data. + * If so, sets the withdraw object with the request data. * Otherwise, it queues an appropriate MHD response. * * @param connection The HTTP connection to the client * @param h_planchets Original commitment value sent with the withdraw request - * @param[out] commitment Data from the original withdraw request + * @param[out] withdraw Data from the original withdraw request * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. * @return #GNUNET_OK if the withdraw request has been found, * #GNUNET_SYSERR if we did not find the request in the DB */ static enum GNUNET_GenericReturnValue -find_original_commitment ( +find_original_withdraw ( struct MHD_Connection *connection, const struct TALER_HashBlindedPlanchetsP *h_planchets, - struct TALER_EXCHANGEDB_Withdraw *commitment, + struct TALER_EXCHANGEDB_Withdraw *withdraw, MHD_RESULT *result) { enum GNUNET_DB_QueryStatus qs; @@ -152,7 +152,7 @@ find_original_commitment ( { qs = TEH_plugin->get_withdraw (TEH_plugin->cls, h_planchets, - commitment); + withdraw); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: @@ -192,6 +192,8 @@ find_original_commitment ( * @param connection Connection to the client * @param denom_key The denomination key * @param secret The secret to a planchet + * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL + * @param nonce The derived nonce needed for CS denomination * @param max_age The maximum age allowed * @param[out] bch Hashcode to write * @param[out] result On error, a HTTP-response will be queued and result set accordingly @@ -203,6 +205,8 @@ calculate_blinded_hash ( struct MHD_Connection *connection, struct TEH_DenominationKey *denom_key, const struct TALER_PlanchetMasterSecretP *secret, + const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub, + union GNUNET_CRYPTO_BlindSessionNonce *nonce, uint8_t max_age, struct TALER_BlindedCoinHashP *bch, MHD_RESULT *result) @@ -235,25 +239,12 @@ calculate_blinded_hash ( struct TALER_ExchangeWithdrawValues alg_values = { .blinding_inputs = &bi }; - union GNUNET_CRYPTO_BlindSessionNonce nonce; - union GNUNET_CRYPTO_BlindSessionNonce *noncep = NULL; - // FIXME[oec?]: add logic to denom.c to do this! if (GNUNET_CRYPTO_BSA_CS == bi.cipher) { - struct TEH_CsDeriveData cdd = { - .h_denom_pub = &denom_key->h_denom_pub, - .nonce = &nonce.cs_nonce, - }; - - TALER_cs_withdraw_nonce_derive (secret, - &nonce.cs_nonce); - noncep = &nonce; - GNUNET_assert (TALER_EC_NONE == - TEH_keys_denomination_cs_r_pub ( - &cdd, - false, - &bi.details.cs_values)); + GNUNET_assert (NULL != r_pub); + GNUNET_assert (NULL != nonce); + bi.details.cs_values = *r_pub; } TALER_planchet_blinding_secret_create (secret, &alg_values, @@ -264,7 +255,7 @@ calculate_blinded_hash ( ret = TALER_planchet_prepare (&denom_key->denom_pub, &alg_values, &bks, - noncep, + nonce, &coin_priv, &ach, &c_hash, @@ -307,31 +298,33 @@ calculate_blinded_hash ( * a coin's private keys is defined in * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw * - * @param connection HTTP-connection to the client - * @param commitment Original commitment + * @param con HTTP-connection to the client + * @param wd Original withdraw request * @param disclosed_batch_seeds The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many * @param[out] result On error, a HTTP-response will be queued and result set accordingly * @return GNUNET_OK on success, GNUNET_SYSERR otherwise */ static enum GNUNET_GenericReturnValue verify_commitment_and_max_age ( - struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_Withdraw *commitment, + struct MHD_Connection *con, + const struct TALER_EXCHANGEDB_Withdraw *wd, const struct TALER_RevealWithdrawMasterSeedsP *disclosed_batch_seeds, MHD_RESULT *result) { enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; struct GNUNET_HashContext *hash_context; struct TEH_KeyStateHandle *keys; - struct TEH_DenominationKey *denom_keys[commitment->num_coins]; + struct TEH_DenominationKey *denom_keys[wd->num_coins]; struct TALER_Amount total_amount; struct TALER_Amount total_fee; struct TALER_AgeMask mask; - struct TALER_PlanchetMasterSecretP - secrets[TALER_CNC_KAPPA - 1][commitment->num_coins]; + struct TALER_PlanchetMasterSecretP secrets[TALER_CNC_KAPPA + - 1][wd->num_coins]; + bool is_cs[wd->num_coins]; + size_t cs_count = 0; uint8_t secrets_idx = 0; /* first index into secrets */ - GNUNET_assert (commitment->noreveal_index < TALER_CNC_KAPPA); + GNUNET_assert (wd->noreveal_index < TALER_CNC_KAPPA); GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, @@ -343,30 +336,31 @@ verify_commitment_and_max_age ( memset (denom_keys, 0, sizeof(denom_keys)); + memset (is_cs, + 0, + sizeof(is_cs)); /* We need the current keys in memory for the meta-data of the denominations */ keys = TEH_keys_get_state (); if (NULL == keys) { - *result = TALER_MHD_reply_with_ec (connection, + *result = TALER_MHD_reply_with_ec (con, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); return GNUNET_SYSERR; } /* Find the denomination keys */ - for (size_t i = 0; i < commitment->num_coins; i++) + for (size_t i = 0; i < wd->num_coins; i++) { denom_keys[i] = - TEH_keys_denomination_by_hash_from_state ( + TEH_keys_denomination_by_serial_from_state ( keys, - &commitment->denom_pub_hashes[i], - connection, - result); + wd->denom_serials[i]); if (NULL == denom_keys[i]) { GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (connection, + *result = TALER_MHD_reply_with_ec (con, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); return GNUNET_SYSERR; @@ -383,67 +377,111 @@ verify_commitment_and_max_age ( if (i == 0) mask = denom_keys[i]->meta.age_mask; GNUNET_assert (mask.bits == denom_keys[i]->meta.age_mask.bits); + + if (GNUNET_CRYPTO_BSA_CS == + denom_keys[i]->denom_pub.bsign_pub_key->cipher) + { + is_cs[i] = true; + cs_count++; + } } hash_context = GNUNET_CRYPTO_hash_context_start (); - for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++) { - /* Expand the secrets for a disclosed batch */ - if (gamma != commitment->noreveal_index) - { - GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1)); - TALER_expand_withdraw_secrets (commitment->num_coins, - &disclosed_batch_seeds->tuple[secrets_idx], - secrets[secrets_idx]); - } + uint32_t cs_indices[cs_count]; + union GNUNET_CRYPTO_BlindSessionNonce nonces[cs_count]; + size_t cs_idx = 0; /* [0...cs_count) */ + + for (size_t i = 0; i < wd->num_coins; i++) + if (is_cs[i]) + cs_indices[cs_idx++] = i; - for (size_t coin_idx = 0; coin_idx < commitment->num_coins; coin_idx++) + TALER_cs_derive_blind_nonces_from_seed (&wd->blinding_seed, + false, /* not for melt */ + cs_count, + cs_indices, + nonces); + + for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++) { - /* Now find or create the actual hash of the blinded planchet */ - if (gamma == commitment->noreveal_index) + cs_idx = 0; + + /* Expand the secrets for a disclosed batch */ + if (gamma != wd->noreveal_index) { - GNUNET_CRYPTO_hash_context_read ( - hash_context, - &commitment->h_coin_evs[coin_idx], - sizeof(commitment->h_coin_evs[coin_idx])); + GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1)); + TALER_expand_withdraw_secrets ( + wd->num_coins, + &disclosed_batch_seeds->tuple[secrets_idx], + secrets[secrets_idx]); } - else /* disclosed case: we need to create the blinded hash ourselves */ - { - struct TALER_BlindedCoinHashP bch; - - ret = calculate_blinded_hash (connection, - denom_keys[coin_idx], - &secrets[secrets_idx][coin_idx], - commitment->max_age, - &bch, - result); - - if (GNUNET_OK != ret) + for (size_t coin_idx = 0; coin_idx < wd->num_coins; coin_idx++) + { + /* Now find or create the actual hash of the blinded planchet */ + if (gamma == wd->noreveal_index) { - GNUNET_CRYPTO_hash_context_abort (hash_context); - return GNUNET_SYSERR; + GNUNET_CRYPTO_hash_context_read ( + hash_context, + &wd->h_coin_evs[coin_idx], + sizeof(wd->h_coin_evs[coin_idx])); } + else /* disclosed case: we need to create the blinded hash ourselves */ + { + struct TALER_BlindedCoinHashP bch; + struct GNUNET_CRYPTO_CSPublicRPairP *rp; + union GNUNET_CRYPTO_BlindSessionNonce *np; + + if (is_cs[coin_idx]) + { + GNUNET_assert (cs_idx < cs_count); + np = &nonces[cs_idx]; + rp = &wd->cs_r_pubs[cs_idx]; + cs_idx++; + } + else + { + np = NULL; + rp = NULL; + } + + ret = calculate_blinded_hash ( + con, + denom_keys[coin_idx], + &secrets[secrets_idx][coin_idx], + rp, + np, + wd->max_age, + &bch, + result); + + + if (GNUNET_OK != ret) + { + GNUNET_CRYPTO_hash_context_abort (hash_context); + return GNUNET_SYSERR; + } + + /* Continue the running hash of all coin hashes with the calculated + * hash-value of the current, disclosed coin */ + GNUNET_CRYPTO_hash_context_read (hash_context, + &bch, + sizeof(bch)); + } + } /* for-loop over num_coins */ - /* Continue the running hash of all coin hashes with the calculated - * hash-value of the current, disclosed coin */ - GNUNET_CRYPTO_hash_context_read (hash_context, - &bch, - sizeof(bch)); - } - } /* for-loop over num_coins */ - - /** - * Only for the disclosed indices do we increment the index - * into secrets. - */ - if (gamma != commitment->noreveal_index) - secrets_idx++; + /** + * Only for the disclosed indices do we increment the index + * into secrets. + */ + if (gamma != wd->noreveal_index) + secrets_idx++; + } } - /* Finally, compare the calculated hash with the original commitment */ + /* Finally, compare the calculated hash with the original wd */ { struct TALER_HashBlindedPlanchetsP h_planchets; @@ -452,11 +490,11 @@ verify_commitment_and_max_age ( &h_planchets.hash); if (0 != GNUNET_CRYPTO_hash_cmp ( - &commitment->h_planchets.hash, + &wd->h_planchets.hash, &h_planchets.hash)) { GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (connection, + *result = TALER_MHD_reply_with_ec (con, TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH, NULL); return GNUNET_SYSERR; @@ -468,7 +506,7 @@ verify_commitment_and_max_age ( /** - * @brief Send a response for "/withdraw/$RCH/reveal" + * @brief Send a response for "/reveal-withdraw" * * @param connection The http connection to the client to send the response to * @param commitment The data from the commitment with signatures @@ -542,10 +580,10 @@ TEH_handler_reveal_withdraw ( /* Find original commitment */ if (GNUNET_OK != - find_original_commitment ( + find_original_withdraw ( rc->connection, &actx.h_planchets, - &actx.commitment, + &actx.withdraw, &result)) break; @@ -554,24 +592,24 @@ TEH_handler_reveal_withdraw ( if (GNUNET_OK != verify_commitment_and_max_age ( rc->connection, - &actx.commitment, + &actx.withdraw, &actx.disclosed_batch_seeds, &result)) break; /* Finally, return the signatures */ result = reply_withdraw_reveal_success (rc->connection, - &actx.commitment); + &actx.withdraw); } while (0); GNUNET_JSON_parse_free (spec); - if (NULL != actx.commitment.denom_sigs) - for (unsigned int i = 0; i<actx.commitment.num_coins; i++) - TALER_blinded_denom_sig_free (&actx.commitment.denom_sigs[i]); - GNUNET_free (actx.commitment.denom_sigs); - GNUNET_free (actx.commitment.denom_pub_hashes); - GNUNET_free (actx.commitment.denom_serials); + if (NULL != actx.withdraw.denom_sigs) + for (unsigned int i = 0; i<actx.withdraw.num_coins; i++) + TALER_blinded_denom_sig_free (&actx.withdraw.denom_sigs[i]); + GNUNET_free (actx.withdraw.denom_sigs); + GNUNET_free (actx.withdraw.denom_pub_hashes); + GNUNET_free (actx.withdraw.denom_serials); return result; } diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c @@ -48,6 +48,7 @@ enum WithdrawError WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED, WITHDRAW_ERROR_AMOUNT_OVERFLOW, WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, + WITHDRAW_ERROR_BLINDING_SEED_REQUIRED, WITHDRAW_ERROR_CIPHER_MISMATCH, WITHDRAW_ERROR_CONFIRMATION_SIGN, WITHDRAW_ERROR_DB_FETCH_FAILED, @@ -60,6 +61,7 @@ enum WithdrawError WITHDRAW_ERROR_FEE_OVERFLOW, WITHDRAW_ERROR_IDEMPOTENT_PLANCHET, WITHDRAW_ERROR_INSUFFICIENT_FUNDS, + WITHDRAW_ERROR_CRYPTO_HELPER, WITHDRAW_ERROR_KEYS_MISSING, WITHDRAW_ERROR_KYC_REQUIRED, WITHDRAW_ERROR_LEGITIMIZATION_RESULT, @@ -158,7 +160,7 @@ struct WithdrawContext * All fields (from the request or computed) * that we persist in the database. */ - struct TALER_EXCHANGEDB_Withdraw persist; + struct TALER_EXCHANGEDB_Withdraw withdraw; /** * In some error cases we check for idempotency. @@ -171,7 +173,7 @@ struct WithdrawContext * for idempotency and the result from the database * is stored here. */ - struct TALER_EXCHANGEDB_Withdraw idem; + struct TALER_EXCHANGEDB_Withdraw withdraw_idem; /** * Array ``num_coins`` of hashes of the public keys @@ -205,6 +207,12 @@ struct WithdrawContext */ struct TALER_Amount fee; + /** + * Array @e withdraw.num_r_pubs of indices into @e denoms_h + * of CS denominations. + */ + uint32_t *cs_indices; + } request; @@ -349,8 +357,8 @@ withdraw_is_idempotent ( { qs = TEH_plugin->get_withdraw ( TEH_plugin->cls, - &wc->request.persist.h_planchets, - &wc->request.idem); + &wc->request.withdraw.h_planchets, + &wc->request.withdraw_idem); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) continue; @@ -407,19 +415,21 @@ withdraw_transaction ( bool balance_ok; bool age_ok; bool conflict; + bool nonce_reuse; uint16_t allowed_maximum_age; uint32_t reserve_birthday; struct TALER_Amount insufficient_funds; qs = TEH_plugin->do_withdraw (TEH_plugin->cls, - &wc->request.persist, + &wc->request.withdraw, wc->now, &balance_ok, &insufficient_funds, &age_ok, &allowed_maximum_age, &reserve_birthday, - &conflict); + &conflict, + &nonce_reuse); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) @@ -439,7 +449,7 @@ withdraw_transaction ( if (! age_ok) { - if (wc->request.persist.age_proof_required) + if (wc->request.withdraw.age_proof_required) { wc->error.details.maximum_age_too_large.max_allowed = allowed_maximum_age; wc->error.details.maximum_age_too_large.birthday = reserve_birthday; @@ -458,11 +468,17 @@ withdraw_transaction ( if (! balance_ok) { TEH_plugin->rollback (TEH_plugin->cls); - SET_ERROR_WITH_FIELD (wc, WITHDRAW_ERROR_INSUFFICIENT_FUNDS, insufficient_funds); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (nonce_reuse) + { + GNUNET_break (0); + SET_ERROR (wc, + WITHDRAW_ERROR_NONCE_RESUSE); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -532,20 +548,18 @@ phase_prepare_transaction ( { size_t offset = 0; - wc->request.persist.denom_sigs + wc->request.withdraw.denom_sigs = GNUNET_new_array ( - wc->request.persist.num_coins, + wc->request.withdraw.num_coins, struct TALER_BlindedDenominationSignature); - - wc->request.persist.h_coin_evs + wc->request.withdraw.h_coin_evs = GNUNET_new_array ( - wc->request.persist.num_coins, + wc->request.withdraw.num_coins, struct TALER_BlindedCoinHashP); - /* Pick the challenge in case of age restriction */ - if (wc->request.persist.age_proof_required) + if (wc->request.withdraw.age_proof_required) { - wc->request.persist.noreveal_index = + wc->request.withdraw.noreveal_index = GNUNET_CRYPTO_random_u32 ( GNUNET_CRYPTO_QUALITY_STRONG, TALER_CNC_KAPPA); @@ -553,15 +567,15 @@ phase_prepare_transaction ( * In case of age restriction, we use the corresponding offset in the planchet * array to the beginning of the coins corresponding to the noreveal_index. */ - offset = wc->request.persist.noreveal_index * wc->request.persist.num_coins; - - GNUNET_assert (offset + wc->request.persist.num_coins <= + offset = wc->request.withdraw.noreveal_index + * wc->request.withdraw.num_coins; + GNUNET_assert (offset + wc->request.withdraw.num_coins <= wc->request.num_planchets); } /* Choose and sign the coins */ { - struct TEH_CoinSignData csds[wc->request.persist.num_coins]; + struct TEH_CoinSignData csds[wc->request.withdraw.num_coins]; enum TALER_ErrorCode ec_denomination_sign; memset (csds, @@ -569,17 +583,17 @@ phase_prepare_transaction ( sizeof(csds)); /* Pick the chosen blinded coins */ - for (uint32_t i = 0; i<wc->request.persist.num_coins; i++) + for (uint32_t i = 0; i<wc->request.withdraw.num_coins; i++) { csds[i].bp = &wc->request.planchets[i + offset]; csds[i].h_denom_pub = &wc->request.denoms_h[i]; } ec_denomination_sign = TEH_keys_denomination_batch_sign ( - wc->request.persist.num_coins, + wc->request.withdraw.num_coins, csds, false, - wc->request.persist.denom_sigs); + wc->request.withdraw.denom_sigs); if (TALER_EC_NONE != ec_denomination_sign) { GNUNET_break (0); @@ -590,14 +604,59 @@ phase_prepare_transaction ( } /* Prepare the hashes of the coins for insertion */ - for (uint32_t i = 0; i<wc->request.persist.num_coins; i++) + for (uint32_t i = 0; i<wc->request.withdraw.num_coins; i++) { TALER_coin_ev_hash (&wc->request.planchets[i + offset], &wc->request.denoms_h[i], - &wc->request.persist.h_coin_evs[i]); + &wc->request.withdraw.h_coin_evs[i]); } } + /** + * For the denominations with cipher CS, we need to calculate the R-values. + */ + if (0 < wc->request.withdraw.num_cs_r_pubs) + { + size_t num_cs_r_pubs = wc->request.withdraw.num_cs_r_pubs; + struct TEH_CsDeriveData cdds[num_cs_r_pubs]; + struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_pubs]; + + memset (nonces, 0, sizeof(nonces)); + wc->request.withdraw.cs_r_pubs + = GNUNET_new_array ( + num_cs_r_pubs, + struct GNUNET_CRYPTO_CSPublicRPairP); + + GNUNET_assert (! wc->request.withdraw.no_blinding_seed); + TALER_cs_derive_nonces_from_seed ( + &wc->request.withdraw.blinding_seed, + false, /* not for melt */ + num_cs_r_pubs, + wc->request.cs_indices, + nonces); + + for (size_t i = 0; i < num_cs_r_pubs; i++) + { + size_t idx = wc->request.cs_indices[i]; + + GNUNET_assert (idx < wc->request.withdraw.num_coins); + cdds[i].h_denom_pub = &wc->request.denoms_h[idx]; + cdds[i].nonce = &nonces[i]; + } + + if (TALER_EC_NONE != + TEH_keys_denomination_cs_batch_r_pub_simple ( + wc->request.withdraw.num_cs_r_pubs, + cdds, + false, + wc->request.withdraw.cs_r_pubs)) + { + GNUNET_break (0); + SET_ERROR (wc, + WITHDRAW_ERROR_CRYPTO_HELPER); + return; + } + } wc->phase++; } @@ -657,20 +716,6 @@ withdraw_legi_cb ( /** - * Helper function to return a string representing the type of withdraw (age or batch). - * - * @param wc withdraw context - */ -static const char * -typ2str ( - const struct WithdrawContext *wc) -{ - return (wc->request.persist.age_proof_required) ? "age-withdraw" : - "batch-withdraw"; -} - - -/** * Function called to iterate over KYC-relevant transaction amounts for a * particular time range. Called within a database transaction, so must * not start a new one. @@ -696,12 +741,11 @@ withdraw_amount_cb ( enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Signaling amount %s for KYC check during %sal\n", - TALER_amount2s (&wc->request.persist.amount_with_fee), - typ2str (wc)); + "Signaling amount %s for KYC check during witdrawal\n", + TALER_amount2s (&wc->request.withdraw.amount_with_fee)); ret = cb (cb_cls, - &wc->request.persist.amount_with_fee, + &wc->request.withdraw.amount_with_fee, wc->now.abs_time); GNUNET_break (GNUNET_SYSERR != ret); @@ -717,9 +761,8 @@ withdraw_amount_cb ( cb_cls); GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got %d additional transactions for this %sal and limit %llu\n", + "Got %d additional transactions for this withdrawal and limit %llu\n", qs, - typ2str (wc), (unsigned long long) limit.abs_value_us); GNUNET_break (qs >= 0); @@ -743,7 +786,7 @@ phase_run_legi_check (struct WithdrawContext *wc) /* Check if the money came from a wire transfer */ qs = TEH_plugin->reserves_get_origin ( TEH_plugin->cls, - &wc->request.persist.reserve_pub, + &wc->request.withdraw.reserve_pub, &h_full_payto, &payto_uri); if (qs < 0) @@ -806,13 +849,11 @@ find_denomination ( struct TEH_DenominationKey *dk; *pdk = NULL; - dk = TEH_keys_denomination_by_hash_from_state ( ksh, denom_h, NULL, NULL); - if (NULL == dk) { SET_ERROR_WITH_FIELD (wc, @@ -820,7 +861,6 @@ find_denomination ( denom_h); return GNUNET_NO; } - if (GNUNET_TIME_absolute_is_past ( dk->meta.expire_withdraw.abs_time)) { @@ -829,7 +869,6 @@ find_denomination ( denom_h); return GNUNET_SYSERR; } - if (GNUNET_TIME_absolute_is_future ( dk->meta.start.abs_time)) { @@ -839,16 +878,14 @@ find_denomination ( denom_h); return GNUNET_SYSERR; } - if (dk->recoup_possible) { SET_ERROR (wc, WITHDRAW_ERROR_DENOMINATION_REVOKED); return GNUNET_SYSERR; } - - /* In case of age withdraw, make sure that the denomitation supports age restriction */ - if (wc->request.persist.age_proof_required) + /* In case of age withdraw, make sure that the denomination supports age restriction */ + if (wc->request.withdraw.age_proof_required) { if (0 == dk->denom_pub.age_mask.bits) { @@ -859,7 +896,6 @@ find_denomination ( return GNUNET_SYSERR; } } - *pdk = dk; return GNUNET_OK; } @@ -879,6 +915,11 @@ phase_check_keys ( struct WithdrawContext *wc) { struct TEH_KeyStateHandle *ksh; + bool is_cs_denom[wc->request.withdraw.num_coins]; + + memset (is_cs_denom, + 0, + sizeof(is_cs_denom)); ksh = TEH_keys_get_state (); if (NULL == ksh) @@ -888,34 +929,43 @@ phase_check_keys ( WITHDRAW_ERROR_KEYS_MISSING); return; } - - wc->request.persist.denom_serials = - GNUNET_new_array (wc->request.persist.num_coins, + wc->request.withdraw.denom_serials = + GNUNET_new_array (wc->request.withdraw.num_coins, uint64_t); - GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &wc->request.amount)); - GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &wc->request.fee)); - GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, - &wc->request.persist.amount_with_fee)); + &wc->request.withdraw.amount_with_fee)); - for (unsigned int i = 0; i < wc->request.persist.num_coins; i++) + for (unsigned int i = 0; i < wc->request.withdraw.num_coins; i++) { struct TEH_DenominationKey *dk; - if (GNUNET_OK != find_denomination ( - wc, - ksh, - &wc->request.denoms_h[i], - &dk)) + if (GNUNET_OK != find_denomination (wc, + ksh, + &wc->request.denoms_h[i], + &dk)) return; + if (GNUNET_CRYPTO_BSA_CS == + dk->denom_pub.bsign_pub_key->cipher) + { + if (wc->request.withdraw.no_blinding_seed) + { + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_BLINDING_SEED_REQUIRED); + return; + } + wc->request.withdraw.num_cs_r_pubs++; + is_cs_denom[i] = true; + } + /* Ensure the ciphers from the planchets match the denominations'. */ { /** @@ -932,12 +982,13 @@ phase_check_keys ( * We try to be smart here and handle both variants in one loop * that either runs only once or kappa times. */ - uint8_t kappa = wc->request.persist.age_proof_required ? TALER_CNC_KAPPA : - 0; + uint8_t kappa = wc->request.withdraw.age_proof_required + ? TALER_CNC_KAPPA + : 1; for (uint8_t k = 0; k < kappa; k++) { - size_t off = k * wc->request.persist.num_coins; + size_t off = k * wc->request.withdraw.num_coins; if (dk->denom_pub.bsign_pub_key->cipher != wc->request.planchets[i + off].blinded_message->cipher) @@ -971,12 +1022,12 @@ phase_check_keys ( WITHDRAW_ERROR_FEE_OVERFLOW); return; } - wc->request.persist.denom_serials[i] = dk->meta.serial; + wc->request.withdraw.denom_serials[i] = dk->meta.serial; } /* Save the total amount including fees */ if (0 > TALER_amount_add ( - &wc->request.persist.amount_with_fee, + &wc->request.withdraw.amount_with_fee, &wc->request.amount, &wc->request.fee)) { @@ -986,6 +1037,21 @@ phase_check_keys ( return; } + /* Save the indices of CS denominations */ + { + size_t j = 0; + + wc->request.cs_indices = GNUNET_new_array ( + wc->request.withdraw.num_cs_r_pubs, + uint32_t); + + for (size_t i = 0; i < wc->request.withdraw.num_coins; i++) + { + if (is_cs_denom[i]) + wc->request.cs_indices[j++] = i; + } + } + wc->phase++; } @@ -1005,14 +1071,18 @@ phase_check_reserve_signature ( TALER_wallet_withdraw_verify ( &wc->request.amount, &wc->request.fee, - &wc->request.persist.h_planchets, - (wc->request.persist.age_proof_required) ? - &TEH_age_restriction_config.mask : - NULL, - (wc->request.persist.age_proof_required) ? wc->request.persist.max_age : - 0, - &wc->request.persist.reserve_pub, - &wc->request.persist.reserve_sig)) + &wc->request.withdraw.h_planchets, + wc->request.withdraw.no_blinding_seed + ? NULL + : &wc->request.withdraw.blinding_seed, + (wc->request.withdraw.age_proof_required) + ? &TEH_age_restriction_config.mask + : NULL, + (wc->request.withdraw.age_proof_required) + ? wc->request.withdraw.max_age + : 0, + &wc->request.withdraw.reserve_pub, + &wc->request.withdraw.reserve_sig)) { GNUNET_break_op (0); SET_ERROR (wc, @@ -1048,22 +1118,26 @@ clean_withdraw_rc (struct TEH_RequestContext *rc) GNUNET_free (wc->request.planchets); - if (NULL != wc->request.persist.denom_sigs) - for (unsigned int i = 0; i<wc->request.persist.num_coins; i++) - TALER_blinded_denom_sig_free (&wc->request.persist.denom_sigs[i]); + if (NULL != wc->request.withdraw.denom_sigs) + for (unsigned int i = 0; i<wc->request.withdraw.num_coins; i++) + TALER_blinded_denom_sig_free (&wc->request.withdraw.denom_sigs[i]); - GNUNET_free (wc->request.persist.denom_sigs); - GNUNET_free (wc->request.persist.h_coin_evs); - GNUNET_free (wc->request.persist.denom_serials); + GNUNET_free (wc->request.withdraw.denom_sigs); + GNUNET_free (wc->request.withdraw.h_coin_evs); + GNUNET_free (wc->request.withdraw.denom_serials); + GNUNET_free (wc->request.withdraw.cs_r_pubs); + GNUNET_free (wc->request.cs_indices); if (wc->request.is_idempotent) { - for (unsigned int i = 0; i<wc->request.idem.num_coins; i++) - TALER_blinded_denom_sig_free (&wc->request.idem.denom_sigs[i]); - - GNUNET_free (wc->request.idem.h_coin_evs); - GNUNET_free (wc->request.idem.denom_sigs); - GNUNET_free (wc->request.idem.denom_serials); + for (unsigned int i = 0; i<wc->request.withdraw_idem.num_coins; i++) + TALER_blinded_denom_sig_free (&wc->request.withdraw_idem.denom_sigs[i]); + + GNUNET_free (wc->request.withdraw_idem.h_coin_evs); + GNUNET_free (wc->request.withdraw_idem.denom_sigs); + GNUNET_free (wc->request.withdraw_idem.denom_serials); + if (0 < wc->request.withdraw_idem.num_cs_r_pubs) + GNUNET_free (wc->request.withdraw_idem.cs_r_pubs); } if (WITHDRAW_ERROR_LEGITIMIZATION_RESULT == wc->error.code && @@ -1088,10 +1162,10 @@ phase_generate_reply_success (struct WithdrawContext *wc) struct TALER_EXCHANGEDB_Withdraw *db_obj; db_obj = wc->request.is_idempotent - ? &wc->request.idem - : &wc->request.persist; + ? &wc->request.withdraw_idem + : &wc->request.withdraw; - if (wc->request.persist.age_proof_required) + if (wc->request.withdraw.age_proof_required) { struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; @@ -1256,7 +1330,7 @@ phase_generate_reply_error ( wc->rc->connection, wc->error.details.denom_h, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - typ2str (wc))); + NULL)); break; case WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: @@ -1265,7 +1339,7 @@ phase_generate_reply_error ( wc->rc->connection, wc->error.details.denom_h, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - typ2str (wc))); + NULL)); break; case WITHDRAW_ERROR_DENOMINATION_REVOKED: @@ -1274,7 +1348,7 @@ phase_generate_reply_error ( TALER_MHD_reply_with_ec ( wc->rc->connection, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - typ2str (wc))); + NULL)); break; case WITHDRAW_ERROR_CIPHER_MISMATCH: @@ -1285,6 +1359,22 @@ phase_generate_reply_error ( NULL)); break; + case WITHDRAW_ERROR_BLINDING_SEED_REQUIRED: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_GENERIC_PARAMETER_MISSING, + "blinding_seed")); + break; + + case WITHDRAW_ERROR_CRYPTO_HELPER: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL)); + break; + case WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN: finish_loop (wc, TALER_MHD_reply_with_ec ( @@ -1370,8 +1460,8 @@ phase_generate_reply_error ( wc->rc->connection, TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, &wc->error.details.insufficient_funds, - &wc->request.persist.amount_with_fee, - &wc->request.persist.reserve_pub)); + &wc->request.withdraw.amount_with_fee, + &wc->request.withdraw.reserve_pub)); break; case WITHDRAW_ERROR_IDEMPOTENT_PLANCHET: @@ -1387,9 +1477,9 @@ phase_generate_reply_error ( finish_loop (wc, TALER_MHD_reply_with_error ( wc->rc->connection, - MHD_HTTP_BAD_REQUEST, + MHD_HTTP_CONFLICT, TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, - NULL)); + "blinding_seed")); break; case WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID: @@ -1433,17 +1523,21 @@ withdraw_new_request ( GNUNET_JSON_spec_string ("cipher", &cipher), GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &wc->request.persist.reserve_pub), + &wc->request.withdraw.reserve_pub), GNUNET_JSON_spec_array_const ("denoms_h", &j_denoms_h), GNUNET_JSON_spec_array_const ("coin_evs", &j_coin_evs), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint16 ("max_age", - &wc->request.persist.max_age), + &wc->request.withdraw.max_age), &no_max_age), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("blinding_seed", + &wc->request.withdraw.blinding_seed), + &wc->request.withdraw.no_blinding_seed), GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &wc->request.persist.reserve_sig), + &wc->request.withdraw.reserve_sig), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; @@ -1466,14 +1560,14 @@ withdraw_new_request ( return GNUNET_SYSERR; } - wc->request.persist.age_proof_required = ! no_max_age; + wc->request.withdraw.age_proof_required = ! no_max_age; - if (wc->request.persist.age_proof_required) + if (wc->request.withdraw.age_proof_required) { /* The age value MUST be on the beginning of an age group */ - if (wc->request.persist.max_age != + if (wc->request.withdraw.max_age != TALER_get_lowest_age (&TEH_age_restriction_config.mask, - wc->request.persist.max_age)) + wc->request.withdraw.max_age)) { GNUNET_break_op (0); SET_ERROR_WITH_DETAIL ( @@ -1512,17 +1606,17 @@ withdraw_new_request ( BAIL_IF (num_coins > TALER_MAX_FRESH_COINS, "maximum number of coins that can be withdrawn has been exceeded") - BAIL_IF ((! wc->request.persist.age_proof_required) && + BAIL_IF ((! wc->request.withdraw.age_proof_required) && (num_coins !=array_size), "denoms_h and coin_evs must be arrays of the same size") - BAIL_IF (wc->request.persist.age_proof_required && + BAIL_IF (wc->request.withdraw.age_proof_required && ((TALER_CNC_KAPPA * num_coins) != array_size), "coin_evs must be an array of length " TALER_CNC_KAPPA_STR "*len(denoms_h)") - wc->request.persist.num_coins = num_coins; + wc->request.withdraw.num_coins = num_coins; wc->request.num_planchets = array_size; error = NULL; @@ -1545,7 +1639,7 @@ withdraw_new_request ( json_t *value; wc->request.denoms_h - = GNUNET_new_array (wc->request.persist.num_coins, + = GNUNET_new_array (wc->request.withdraw.num_coins, struct TALER_DenominationHashP); json_array_foreach (j_denoms_h, idx, value) { @@ -1600,7 +1694,7 @@ withdraw_new_request ( TALER_coin_ev_hash ( &wc->request.planchets[idx], - &wc->request.denoms_h[idx % wc->request.persist.num_coins], + &wc->request.denoms_h[idx % wc->request.withdraw.num_coins], &bch); GNUNET_CRYPTO_hash_context_read (hash_context, &bch, @@ -1628,7 +1722,7 @@ withdraw_new_request ( /* Finally, calculate the hash from all blinded envelopes */ GNUNET_CRYPTO_hash_context_finish (hash_context, - &wc->request.persist.h_planchets.hash); + &wc->request.withdraw.h_planchets.hash); } /* scope of hash_context */ return GNUNET_OK; @@ -1663,8 +1757,10 @@ TEH_handler_withdraw ( while (true) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "%s processing in phase %d\n", - typ2str (wc), + "withdrawal%s processing in phase %d\n", + wc->request.withdraw.age_proof_required + ? " (with required age proof)" + : "", wc->phase); switch (wc->phase) { diff --git a/src/exchangedb/0009-withdraw.sql b/src/exchangedb/0009-withdraw.sql @@ -27,7 +27,7 @@ BEGIN PERFORM create_partitioned_table( 'CREATE TABLE %I' '(withdraw_id BIGINT GENERATED BY DEFAULT AS IDENTITY' - ',h_planchets BYTEA NOT NULL CONSTRAINT h_planchets_length CHECK(LENGTH(h_planchets)=64)' + ',h_planchets BYTEA CONSTRAINT h_planchets_length CHECK(LENGTH(h_planchets)=64)' ',execution_date INT8 NOT NULL' ',amount_with_fee taler_amount NOT NULL' ',reserve_pub BYTEA NOT NULL CONSTRAINT reserve_pub_length CHECK(LENGTH(reserve_pub)=32)' @@ -35,6 +35,8 @@ BEGIN ',max_age SMALLINT CONSTRAINT max_age_positive CHECK(max_age>=0)' ',noreveal_index SMALLINT CONSTRAINT noreveal_index_positive CHECK(noreveal_index>=0)' ',h_blind_evs BYTEA[] NOT NULL CONSTRAINT h_blind_evs_length CHECK(cardinality(h_blind_evs)=cardinality(denom_serials))' + ',blinding_seed BYTEA' + ',cs_r_pubs BYTEA[]' ',denom_serials INT8[] NOT NULL CONSTRAINT denom_serials_array_length CHECK(cardinality(denom_serials)=cardinality(denom_sigs))' ',denom_sigs BYTEA[] NOT NULL CONSTRAINT denom_sigs_array_length CHECK(cardinality(denom_sigs)=cardinality(denom_serials))' ') %s ;' @@ -102,6 +104,18 @@ BEGIN ,table_name ,partition_suffix ); + PERFORM comment_partitioned_column( + 'Seed for the blinding of coins for denominations of type Clause-Schnorr, maybe NULL' + ,'blinding_seed' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The calculated R-values (by the exchange) for the coins of cipher type Clause-Schnorr, based on the blinding_seed; maybe NULL.' + ,'cs_r_pubs' + ,table_name + ,partition_suffix + ); END $$; @@ -130,6 +144,11 @@ BEGIN ' ADD CONSTRAINT ' || table_name || '_withdraw_id_key' ' UNIQUE (withdraw_id);' ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_blinding_seed' + ' UNIQUE (blinding_seed);' + ); END $$; @@ -196,5 +215,5 @@ VALUES ('withdraw', 'exchange-0009', 'create', TRUE ,FALSE), ('withdraw', 'exchange-0009', 'constrain',TRUE ,FALSE), ('withdraw', 'exchange-0009', 'foreign', TRUE ,FALSE), - ('withdraw', 'exchange-0009', 'master', TRUE ,FALSE); + ('withdraw', 'exchange-0009', 'master', TRUE ,FALSE); diff --git a/src/exchangedb/exchange_do_withdraw.sql b/src/exchangedb/exchange_do_withdraw.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2023 Taler Systems SA +-- Copyright (C) 2023-2025 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software @@ -18,24 +18,27 @@ DROP FUNCTION IF EXISTS exchange_do_withdraw; CREATE FUNCTION exchange_do_withdraw( - IN amount_with_fee taler_amount, - IN rpub BYTEA, - IN rsig BYTEA, - IN now INT8, - IN min_reserve_gc INT8, - IN h_planchets BYTEA, - IN maximum_age_committed INT2, -- in years ϵ [0,1..) - IN noreveal_index INT2, - IN blinded_evs BYTEA[], - IN denom_serials INT8[], - IN denom_sigs BYTEA[], + IN in_amount_with_fee taler_amount, + IN in_rpub BYTEA, + IN in_rsig BYTEA, + IN in_now INT8, + IN in_min_reserve_gc INT8, + IN in_h_planchets BYTEA, + IN in_maximum_age_committed INT2, -- in years ϵ [0,1..) + IN in_noreveal_index INT2, + IN in_blinded_evs BYTEA[], + IN in_denom_serials INT8[], + IN in_denom_sigs BYTEA[], + IN in_blinding_seed BYTEA, + IN in_cs_r_pubs BYTEA[], OUT reserve_found BOOLEAN, OUT balance_ok BOOLEAN, OUT reserve_balance taler_amount, OUT age_ok BOOLEAN, OUT required_age INT2, -- in years ϵ [0,1..) OUT reserve_birthday INT4, - OUT conflict BOOLEAN) + OUT conflict BOOLEAN, + OUT nonce_reuse BOOLEAN) LANGUAGE plpgsql AS $$ DECLARE @@ -49,14 +52,13 @@ BEGIN -- reserves_out (INSERT, with CONFLICT detection) by wih -- reserves by reserve_pub (UPDATE) -- reserves_in by reserve_pub (SELECT) --- wire_targets by wire_target_h_payto SELECT current_balance ,birthday ,gc_date INTO reserve FROM reserves - WHERE reserve_pub=rpub; + WHERE reserve_pub=in_rpub; IF NOT FOUND THEN @@ -67,11 +69,13 @@ THEN reserve_balance.val = 0; reserve_balance.frac = 0; balance_ok=FALSE; + nonce_reuse=FALSE; RETURN; END IF; reserve_found = TRUE; conflict=FALSE; -- not really yet determined +nonce_reuse=FALSE; -- also not yet determined reserve_balance = reserve.current_balance; reserve_birthday = reserve.birthday; @@ -80,7 +84,7 @@ reserve_birthday = reserve.birthday; IF (reserve.birthday <> 0) THEN not_before=date '1970-01-01' + reserve.birthday; - earliest_date = current_date - make_interval(maximum_age_committed); + earliest_date = current_date - make_interval(in_maximum_age_committed); -- -- 1970-01-01 + birthday == not_before now -- | | | @@ -91,7 +95,7 @@ THEN -- earliest_date == -- now - maximum_age_committed*year -- - IF ((maximum_age_committed IS NULL) OR + IF ((in_maximum_age_committed IS NULL) OR (earliest_date < not_before)) THEN required_age = extract(year from age(current_date, not_before)); @@ -108,7 +112,7 @@ required_age=0; SELECT * INTO difference FROM amount_left_minus_right(reserve_balance - ,amount_with_fee); + ,in_amount_with_fee); balance_ok = difference.ok; @@ -120,16 +124,16 @@ END IF; balance = difference.diff; -- Calculate new expiration dates. -min_reserve_gc=GREATEST(min_reserve_gc,reserve.gc_date); +in_min_reserve_gc=GREATEST(in_min_reserve_gc,reserve.gc_date); -- Update reserve balance. UPDATE reserves SET - gc_date=min_reserve_gc + gc_date=in_min_reserve_gc ,current_balance=balance WHERE - reserve_pub=rpub; + reserve_pub=in_rpub; --- Write the commitment into the withdraw table +-- Write the data into the withdraw table INSERT INTO withdraw (h_planchets ,execution_date @@ -140,31 +144,70 @@ INSERT INTO withdraw ,noreveal_index ,denom_serials ,h_blind_evs + ,blinding_seed + ,cs_r_pubs ,denom_sigs) VALUES - (h_planchets - ,now - ,maximum_age_committed - ,amount_with_fee - ,rpub - ,rsig - ,noreveal_index - ,denom_serials - ,blinded_evs - ,denom_sigs) + (in_h_planchets + ,in_now + ,in_maximum_age_committed + ,in_amount_with_fee + ,in_rpub + ,in_rsig + ,in_noreveal_index + ,in_denom_serials + ,in_blinded_evs + ,in_blinding_seed + ,in_cs_r_pubs + ,in_denom_sigs) ON CONFLICT DO NOTHING; +-- We have a conflict due to either +-- the h_planchets, (h_planchets, reserve_pub) +-- or the blinding_seed. IF NOT FOUND THEN + -- First check for blinding_seed reuse + IF in_blinding_seed IS NOT NULL + THEN + SELECT TRUE + FROM withdraw + INTO nonce_reuse + WHERE blinding_seed = in_blinding_seed + AND h_planchets <> in_h_planchets; + + IF nonce_reuse + THEN + conflict=FALSE; + RETURN; + END IF; + END IF; + -- Signal a conflict so that the caller -- can fetch the actual data from the DB. conflict=TRUE; + nonce_reuse=FALSE; RETURN; -ELSE - conflict=FALSE; END IF; +conflict=FALSE; +nonce_reuse=FALSE; +RETURN; + END $$; -COMMENT ON FUNCTION exchange_do_withdraw(taler_amount, BYTEA, BYTEA, INT8, INT8, BYTEA, INT2, INT2, BYTEA[], INT8[], BYTEA[]) - IS 'Checks whether the reserve has sufficient balance for an withdraw operation (or the request is repeated and was previously approved) and that age requirements are met. If so updates the database with the result. Includes storing the blinded planchets and denomination signatures, or signaling conflict'; +COMMENT ON FUNCTION exchange_do_withdraw( + taler_amount, + BYTEA, + BYTEA, + INT8, + INT8, + BYTEA, + INT2, + INT2, + BYTEA[], + INT8[], + BYTEA[], + BYTEA, + BYTEA[]) + IS 'Checks whether the reserve has sufficient balance for an withdraw operation (or the request is repeated and was previously approved) and that age requirements are met. If so updates the database with the result. Includes storing the blinded planchets and denomination signatures, or signaling conflict or nonce reuse'; diff --git a/src/exchangedb/pg_do_withdraw.c b/src/exchangedb/pg_do_withdraw.c @@ -39,7 +39,8 @@ TEH_PG_do_withdraw ( bool *age_ok, uint16_t *required_age, uint32_t *reserve_birthday, - bool *conflict) + bool *conflict, + bool *nonce_reuse) { struct PostgresClosure *pg = cls; struct GNUNET_TIME_Timestamp gc; @@ -66,6 +67,14 @@ TEH_PG_do_withdraw ( TALER_PQ_query_param_array_blinded_denom_sig (withdraw->num_coins, withdraw->denom_sigs, pg->conn), + withdraw->no_blinding_seed + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_auto_from_type (&withdraw->blinding_seed), + (0 < withdraw->num_cs_r_pubs) + ? TALER_PQ_query_param_array_cs_r_pub (withdraw->num_cs_r_pubs, + withdraw->cs_r_pubs, + pg->conn) + : GNUNET_PQ_query_param_null (), GNUNET_PQ_query_param_end }; bool found; @@ -84,10 +93,15 @@ TEH_PG_do_withdraw ( reserve_birthday), GNUNET_PQ_result_spec_bool ("conflict", conflict), + GNUNET_PQ_result_spec_bool ("nonce_reuse", + nonce_reuse), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (withdraw->no_blinding_seed == + (0 == withdraw->num_cs_r_pubs)); + gc = GNUNET_TIME_absolute_to_timestamp ( GNUNET_TIME_absolute_add (now.abs_time, pg->legal_reserve_expiration_time)); @@ -101,8 +115,9 @@ TEH_PG_do_withdraw ( ",required_age" ",reserve_birthday" ",conflict" + ",nonce_reuse" " FROM exchange_do_withdraw" - " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);"); + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "call_withdraw", params, @@ -113,5 +128,5 @@ TEH_PG_do_withdraw ( return qs; if (! found) return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + return qs; } diff --git a/src/exchangedb/pg_do_withdraw.h b/src/exchangedb/pg_do_withdraw.h @@ -30,26 +30,28 @@ * the withdrawal details. * * @param cls the `struct PostgresClosure` with the plugin-specific state - * @param withdraw the request with all parameters - * @param now current time (rounded) + * @param withdraw the withdraw data + * @param now the timestamp of the withdraw * @param[out] balance_ok set to true if the balance was sufficient - * @param[out] reserve_balance set to the original reserve balance (at the start of this transaction) - * @param[out] age_ok set to true if no age requirements are present on the reserve - * @param[out] required_age if @e age_ok is false, set to the maximum allowed age when withdrawing from this reserve - * @param[out] reserve_birthday if @e age_ok is false, set to the birthday of the reserve - * @param[out] conflict set to true if there already is an entry in the database for the given pair (h_commitment, reserve_pub) + * @param[out] reserve_balance set to original balance of the reserve + * @param[out] age_ok set to true if age requirements were met + * @param[out] allowed_maximum_age if @e age_ok is FALSE, this is set to the allowed maximum age + * @param[out] reserve_birthday if @e age_ok is FALSE, this is set to the reserve's birthday + * @param[out] conflict set to true if an entry already exists for the given h_planchets or blinding_seed + * @param[out] nonce_reuse set to true if the blinding_seed has been found in the table for a different withdraw * @return 0 if no reserve was found, 1 if a reserve was found, else the query execution status */ enum GNUNET_DB_QueryStatus TEH_PG_do_withdraw ( void *cls, const struct TALER_EXCHANGEDB_Withdraw *withdraw, - const struct GNUNET_TIME_Timestamp now, + struct GNUNET_TIME_Timestamp now, bool *balance_ok, struct TALER_Amount *reserve_balance, bool *age_ok, - uint16_t *required_age, + uint16_t *allowed_maximum_age, uint32_t *reserve_birthday, - bool *conflict); + bool *conflict, + bool *nonce_reuse); #endif diff --git a/src/exchangedb/pg_get_reserve_history.c b/src/exchangedb/pg_get_reserve_history.c @@ -191,6 +191,7 @@ add_withdraw (void *cls, bool no_max_age; size_t num_h_coin_evs; size_t num_denom_hs; + size_t num_denom_serials; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_auto_from_type ("h_planchets", &wd->h_planchets), @@ -203,6 +204,10 @@ add_withdraw (void *cls, &wd->max_age), &no_max_age), GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("blinding_seed", + &wd->blinding_seed), + &wd->no_blinding_seed), + GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_uint16 ("noreveal_index", &wd->noreveal_index), &no_noreveal_index), @@ -214,6 +219,10 @@ add_withdraw (void *cls, "denom_pub_hashes", &num_denom_hs, &wd->denom_pub_hashes), + GNUNET_PQ_result_spec_array_uint64 (pg->conn, + "denom_serials", + &num_denom_serials, + &wd->denom_serials), GNUNET_PQ_result_spec_end }; @@ -228,6 +237,15 @@ add_withdraw (void *cls, return; } + if ((num_denom_hs != num_denom_serials) || + (num_denom_hs != num_h_coin_evs)) + { + GNUNET_break (0); + GNUNET_free (wd); + rhc->failed = true; + return; + } + if (no_noreveal_index != no_max_age) { GNUNET_break (0); @@ -839,6 +857,8 @@ TEH_PG_get_reserve_history ( ",max_age" ",noreveal_index" ",h_blind_evs" + ",blinding_seed" + ",denom_serials" ",ARRAY(" " SELECT denominations.denom_pub_hash FROM (" " SELECT UNNEST(denom_serials) AS id," @@ -849,7 +869,7 @@ TEH_PG_get_reserve_history ( " FROM withdraw " " WHERE withdraw_id=$2" " AND reserve_pub=$1;"); -#pragma message "get_reserves_out must be removed, once reserves_out is removed" +/* FIXME: remove "get_reserves_out" etc. once reserves_out is phased out */ PREPARE (pg, "get_reserves_out", "SELECT" diff --git a/src/exchangedb/pg_get_withdraw.c b/src/exchangedb/pg_get_withdraw.c @@ -35,9 +35,10 @@ TEH_PG_get_withdraw ( enum GNUNET_DB_QueryStatus ret; struct PostgresClosure *pg = cls; size_t num_sigs; - size_t num_hashes; + size_t num_serials; bool no_noreveal_index; bool no_max_age; + bool no_cs_r_pubs; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (wch), GNUNET_PQ_query_param_end @@ -45,6 +46,10 @@ TEH_PG_get_withdraw ( struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_auto_from_type ("h_planchets", &wd->h_planchets), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("blinding_seed", + &wd->blinding_seed), + &wd->no_blinding_seed), GNUNET_PQ_result_spec_auto_from_type ("reserve_sig", &wd->reserve_sig), GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", @@ -60,22 +65,28 @@ TEH_PG_get_withdraw ( GNUNET_PQ_result_spec_uint16 ("noreveal_index", &wd->noreveal_index), &no_noreveal_index), - TALER_PQ_result_spec_array_blinded_coin_hash ( pg->conn, "h_blind_evs", &wd->num_coins, &wd->h_coin_evs), + GNUNET_PQ_result_spec_allow_null ( + TALER_PQ_result_spec_array_cs_r_pub ( + pg->conn, + "cs_r_pubs", + &wd->num_cs_r_pubs, + &wd->cs_r_pubs), + &no_cs_r_pubs), TALER_PQ_result_spec_array_blinded_denom_sig ( pg->conn, "denom_sigs", &num_sigs, &wd->denom_sigs), - TALER_PQ_result_spec_array_denom_hash ( + GNUNET_PQ_result_spec_array_uint64 ( pg->conn, - "denom_pub_hashes", - &num_hashes, - &wd->denom_pub_hashes), + "denom_serials", + &num_serials, + &wd->denom_serials), GNUNET_PQ_result_spec_end }; @@ -83,20 +94,16 @@ TEH_PG_get_withdraw ( "get_withdraw", "SELECT" " h_planchets" + ",blinding_seed" ",reserve_sig" ",reserve_pub" ",max_age" ",amount_with_fee" ",noreveal_index" ",h_blind_evs" + ",cs_r_pubs" ",denom_sigs" - ",ARRAY(" - " SELECT denominations.denom_pub_hash FROM (" - " SELECT UNNEST(denom_serials) AS id," - " generate_subscripts(denom_serials, 1) AS nr" /* for order */ - " ) AS denoms" - " LEFT JOIN denominations ON denominations.denominations_serial=denoms.id" - ") AS denom_pub_hashes" + ",denom_serials" " FROM withdraw" " WHERE h_planchets=$1;"); @@ -118,19 +125,31 @@ TEH_PG_get_withdraw ( return GNUNET_DB_STATUS_HARD_ERROR; } - wd->age_proof_required = ! no_max_age; + if (wd->no_blinding_seed != no_cs_r_pubs) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } if ((wd->num_coins != num_sigs) || - (wd->num_coins != num_hashes)) + (wd->num_coins != num_serials)) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "got inconsistent number of entries from DB: " - "num_coins=%ld, num_sigs=%ld, num_hashes=%ld\n", + "num_coins=%ld, num_sigs=%ld, num_serials=%ld\n", wd->num_coins, num_sigs, - num_hashes); + num_serials); return GNUNET_DB_STATUS_HARD_ERROR; } + + wd->age_proof_required = ! no_max_age; + if (no_cs_r_pubs) + { + wd->cs_r_pubs = NULL; + wd->num_cs_r_pubs = 0; + } + return ret; } diff --git a/src/exchangedb/pg_insert_records_by_table.c b/src/exchangedb/pg_insert_records_by_table.c @@ -1,6 +1,6 @@ /* This file is part of GNUnet - Copyright (C) 2020-2024 Taler Systems SA + Copyright (C) 2020-2025 Taler Systems SA GNUnet is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -2285,6 +2285,16 @@ irbt_cb_table_withdraw ( &td->details.withdraw.reserve_sig), GNUNET_PQ_query_param_timestamp ( &td->details.withdraw.execution_date), + td->details.withdraw.no_blinding_seed + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_auto_from_type ( + &td->details.withdraw.blinding_seed), + (0 < td->details.withdraw.num_cs_r_pubs) + ? TALER_PQ_query_param_array_cs_r_pub ( + td->details.withdraw.num_cs_r_pubs, + td->details.withdraw.cs_r_pubs, + pg->conn) + : GNUNET_PQ_query_param_null (), GNUNET_PQ_query_param_end }; @@ -2299,8 +2309,10 @@ irbt_cb_table_withdraw ( ",reserve_pub" ",reserve_sig" ",execution_date" + ",blinding_seed" + ",cs_r_pubs" ") VALUES " - "($1, $2, $3, $4, $5, $6, $7, $8);"); + "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_into_table_withdraw", params); diff --git a/src/exchangedb/pg_lookup_records_by_table.c b/src/exchangedb/pg_lookup_records_by_table.c @@ -2664,7 +2664,9 @@ lrbt_cb_table_withdraw (void *cls, { bool no_max_age; bool no_noreveal_index; - bool no_h_planchets; + bool no_cs_r_pubs; + uint64_t num_h_coin_evs; + uint64_t num_sigs; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ( "withdraw_id", @@ -2675,17 +2677,15 @@ lrbt_cb_table_withdraw (void *cls, TALER_PQ_RESULT_SPEC_AMOUNT ( "amount_with_fee", &td.details.withdraw.amount_with_fee), + GNUNET_PQ_result_spec_timestamp ( + "execution_date", + &td.details.withdraw.execution_date), GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_uint16 ( "max_age", &td.details.withdraw.max_age), &no_max_age), GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_auto_from_type ( - "h_planchets", - &td.details.withdraw.h_planchets), - &no_h_planchets), - GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_uint32 ( "noreveal_index", &td.details.withdraw.noreveal_index), @@ -2696,9 +2696,33 @@ lrbt_cb_table_withdraw (void *cls, GNUNET_PQ_result_spec_auto_from_type ( "reserve_sig", &td.details.withdraw.reserve_sig), - GNUNET_PQ_result_spec_timestamp ( - "execution_date", - &td.details.withdraw.execution_date), + GNUNET_PQ_result_spec_array_uint64 ( + pg->conn, + "denom_serials", + &td.details.withdraw.num_coins, + &td.details.withdraw.denominations_serials), + TALER_PQ_result_spec_array_blinded_coin_hash ( + pg->conn, + "h_blind_evs", + &num_h_coin_evs, + &td.details.withdraw.h_coin_evs), + TALER_PQ_result_spec_array_blinded_denom_sig ( + pg->conn, + "denom_sigs", + &num_sigs, + &td.details.withdraw.denom_sigs), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ( + "blinding_seed", + &td.details.withdraw.blinding_seed), + &td.details.withdraw.no_blinding_seed), + GNUNET_PQ_result_spec_allow_null ( + TALER_PQ_result_spec_array_cs_r_pub ( + pg->conn, + "cs_r_pubs", + &td.details.withdraw.num_cs_r_pubs, + &td.details.withdraw.cs_r_pubs), + &no_cs_r_pubs), GNUNET_PQ_result_spec_end }; @@ -2712,15 +2736,32 @@ lrbt_cb_table_withdraw (void *cls, return; } - if ((no_max_age != no_noreveal_index) || - (no_max_age != no_h_planchets)) + if ((num_h_coin_evs != td.details.withdraw.num_coins) || + (num_h_coin_evs != num_sigs)) { GNUNET_break (0); ctx->error = true; + GNUNET_PQ_cleanup_result (rs); + return; + } + + if (no_max_age != no_noreveal_index) + { + GNUNET_break (0); + ctx->error = true; + GNUNET_PQ_cleanup_result (rs); + return; + } + + if (no_cs_r_pubs != td.details.withdraw.no_blinding_seed) + { + GNUNET_break (0); + ctx->error = true; + GNUNET_PQ_cleanup_result (rs); return; } - td.details.withdraw.age_proof_required = ! no_max_age; + td.details.withdraw.age_proof_required = ! no_max_age; ctx->cb (ctx->cb_cls, &td); GNUNET_PQ_cleanup_result (rs); diff --git a/src/exchangedb/pg_select_withdrawals_above_serial_id.c b/src/exchangedb/pg_select_withdrawals_above_serial_id.c @@ -80,12 +80,14 @@ withdraw_serial_helper_cb (void *cls, struct TALER_ReserveSignatureP reserve_sig; struct GNUNET_TIME_Timestamp execution_date; struct TALER_HashBlindedPlanchetsP h_planchets; + struct TALER_BlindingMasterSeedP blinding_seed; struct TALER_Amount amount_with_fee; uint64_t rowid; uint16_t max_age; uint16_t noreveal_index; bool no_noreveal_index; bool no_max_age; + bool no_blinding_seed; struct GNUNET_PQ_ResultSpec rs[] = { TALER_PQ_result_spec_array_blinded_coin_hash (pg->conn, "h_blind_evs", @@ -115,6 +117,10 @@ withdraw_serial_helper_cb (void *cls, GNUNET_PQ_result_spec_uint16 ("noreveal_index", &noreveal_index), &no_noreveal_index), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("blinding_seed", + &blinding_seed), + &no_blinding_seed), GNUNET_PQ_result_spec_end }; enum GNUNET_GenericReturnValue ret; @@ -157,6 +163,7 @@ withdraw_serial_helper_cb (void *cls, h_blind_evs, denom_serials, &h_planchets, + no_blinding_seed ? NULL : &blinding_seed, ! no_max_age, (uint8_t) max_age, (uint8_t) noreveal_index, @@ -197,6 +204,7 @@ TEH_PG_select_withdrawals_above_serial_id ( "SELECT" " withdraw_id" ",h_planchets" + ",blinding_seed" ",h_blind_evs" ",max_age" ",noreveal_index" diff --git a/src/exchangedb/test_exchangedb.c b/src/exchangedb/test_exchangedb.c @@ -721,6 +721,7 @@ audit_reserve_in_cb (void *cls, * @param h_blind_evs array @e num_evs of blinded hashes of the coin's public keys * @param denom_serials array @e num_evs of serial ids of denominations * @param h_planchets running hash over all hashes of blinded planchets in the original withdraw request + * @param blinding_seed seed provided during withdraw, for CS denominations; might be NULL * @param age_proof_required true if the withdraw request required an age proof. * @param max_age if @e age_proof_required is true, the maximum age that was set on the coins. * @param noreveal_index if @e age_proof_required is true, the index that was returned by the exchange for the reveal phase. @@ -737,6 +738,7 @@ audit_reserve_out_cb (void *cls, const struct TALER_BlindedCoinHashP *h_blind_evs, const uint64_t *denom_serials, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, bool age_proof_required, uint8_t max_age, uint8_t noreveal_index, @@ -748,6 +750,8 @@ audit_reserve_out_cb (void *cls, (void) cls; (void) rowid; (void) h_blind_evs; + (void) h_planchets; + (void) blinding_seed; (void) denom_serials; (void) reserve_pub; (void) reserve_sig; @@ -1429,39 +1433,96 @@ run (void *cls) bool balance_ok; bool age_ok; bool conflict; + bool nonce_reuse; uint16_t maximum_age; uint32_t reserve_birthday; uint64_t denom_serial = 1; /* bold assumption */ struct TALER_Amount reserve_balance; - struct TALER_EXCHANGEDB_Withdraw commitment = { + struct TALER_BlindingMasterSeedP blinding_seed = {0}; + struct GNUNET_CRYPTO_CSPublicRPairP cs_r_pubs = {0}; + struct TALER_EXCHANGEDB_Withdraw withdraw_in = { .amount_with_fee = global_value, - .age_proof_required = false, + .age_proof_required = true, .max_age = 0, .noreveal_index = 0, .reserve_pub = reserve_pub, .reserve_sig = cbc.reserve_sig, .h_planchets = h_planchets, + .no_blinding_seed = false, + .blinding_seed = blinding_seed, .num_coins = 1, .h_coin_evs = &cbc.h_coin_envelope, .denom_sigs = &cbc.sig, .denom_serials = &denom_serial, + .num_cs_r_pubs = 1, + .cs_r_pubs = &cs_r_pubs, }; - + struct TALER_Amount zero_amount; FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->do_withdraw (plugin->cls, - &commitment, + &withdraw_in, now, &balance_ok, &reserve_balance, &age_ok, &maximum_age, &reserve_birthday, - &conflict)); - + &conflict, + &nonce_reuse)); GNUNET_assert (! conflict); + GNUNET_assert (! nonce_reuse); GNUNET_assert (balance_ok); + + /** + * Set the amount in the withdraw to zero, + * to avoid triggering balance_ok issues for + * the conflict and nonce_reuse tests. + */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":0.000000", + &zero_amount)); + withdraw_in.amount_with_fee = zero_amount; + + /** + * Change some values to trigger conflict + * due to h_planchet, not nonce. + */ + withdraw_in.blinding_seed.key_data[0] = 1; + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->do_withdraw (plugin->cls, + &withdraw_in, + now, + &balance_ok, + &reserve_balance, + &age_ok, + &maximum_age, + &reserve_birthday, + &conflict, + &nonce_reuse)); + GNUNET_assert (! nonce_reuse); + GNUNET_assert (conflict); + + /** + * Make h_planchet unique again, but trigger + * conflict with blinding_seed. + */ + withdraw_in.blinding_seed.key_data[0] = 0; + withdraw_in.h_planchets.hash.bits[0] += 1; + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->do_withdraw (plugin->cls, + &withdraw_in, + now, + &balance_ok, + &reserve_balance, + &age_ok, + &maximum_age, + &reserve_birthday, + &conflict, + &nonce_reuse)); + GNUNET_assert (! conflict); + GNUNET_assert (nonce_reuse); } FAILIF (GNUNET_OK != diff --git a/src/include/taler_crypto_lib.h b/src/include/taler_crypto_lib.h @@ -807,6 +807,19 @@ struct TALER_RefreshMasterSecretP }; +/** + * Master key material for the deriviation of + * blinding factors. + */ +struct TALER_BlindingMasterSeedP +{ + + /** + * Key material. + */ + uint32_t key_data[8]; + +}; /** * Hash used to represent a denomination public key @@ -1572,7 +1585,7 @@ TALER_planchet_setup_coin_priv ( /** * @brief Method to derive withdraw /csr nonce * - * @param ps planchet secrets of the coin + * @param ps seed for the coins' planchet * @param[out] nonce withdraw nonce included in the request to generate R_0 and R_1 */ void @@ -1582,6 +1595,70 @@ TALER_cs_withdraw_nonce_derive ( /** + * @brief Method to derive a seed for blinding from a seed for withdraw + * + * @param seed input withdraw seed + * @param[out] blinding_seed derived blinding seed + */ +void +TALER_cs_withdraw_seed_to_blinding_seed ( + const struct TALER_WithdrawMasterSeedP *seed, + struct TALER_BlindingMasterSeedP *blinding_seed); + + +/** + * @brief Method to derive a /blinding-prepare nonce for a particular index + * + * @param seed blinding master seed for the nonce + * @param for_melt true if this nonce is for a melt operation + * @param index of the particular coin (in the array of coins in the actual operation) + * @param[out] nonce nonce included in the request to generate R_0 and R_1 + */ +void +TALER_cs_nonce_derive_indexed ( + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, + uint32_t index, + struct GNUNET_CRYPTO_CsSessionNonce *nonce); + + +/** + * @brief Method to derive a batch of CS nonces + * + * @param seed blinding master seed for the nonces + * @param for_melt true if the nonces are for a melt operation + * @param num number of nonces to derive + * @param indices array @e num of coin indices, which contribute to the corresponding nonce + * @param[out] nonces array @e num of nonces + */ +void +TALER_cs_derive_nonces_from_seed ( + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, + size_t num, + const uint32_t indices[static num], + struct GNUNET_CRYPTO_CsSessionNonce nonces[static num]); + + +/** + * @brief Method to derive a batch of blind session nonces + * + * @param seed blinding master seed for the nonces + * @param for_melt true if the nonces are for a melt operation + * @param num number of nonces to derive + * @param indices array @e num of coin indices, which contribute to the corresponding nonce + * @param[out] nonces array @e num of nonces + */ +void +TALER_cs_derive_blind_nonces_from_seed ( + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, + size_t num, + const uint32_t indices[static num], + union GNUNET_CRYPTO_BlindSessionNonce nonces[static num]); + + +/** * @brief Method to derive /csr nonce * to be used during refresh/melt operation. * @@ -2181,7 +2258,7 @@ TALER_withdraw_master_seed_setup_random ( * @param[out] tuple tuples of secrets to fill */ void -TALER_expand_seed_to_kappa_seeds ( +TALER_expand_seed_to_kappa_seed ( const struct TALER_WithdrawMasterSeedP *seed, struct TALER_KappaWithdrawMasterSeedP *tuple); @@ -4178,6 +4255,7 @@ TALER_wallet_blinded_planchets_hash ( * @param amount total amount to withdraw, excluding fees * @param fee total amount of fees * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param blinding_seed blinding_seed used in a prior call to /blinding-prepare, if any of the denomination is of cipher type Clause-Schnorr, might be NULL * @param mask age mask to apply, or NULL, if not applicable. * @param max_age maximum age (in years) to commit to. Must be 0 if age restriction does not apply * @param reserve_priv private key to sign with @@ -4188,6 +4266,7 @@ TALER_wallet_withdraw_sign ( const struct TALER_Amount *amount, const struct TALER_Amount *fee, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePrivateKeyP *reserve_priv, @@ -4200,6 +4279,7 @@ TALER_wallet_withdraw_sign ( * @param amount total amount to withdraw, excluding fees * @param fee total amount of fees * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param blinding_seed blinding_seed used in a prior call to /blinding-prepare, if any of the denomination is of cipher type Clause-Schnorr, might be NULL * @param reserve_priv private key to sign with * @param reserve_sig resulting signature * @return #GNUNET_OK if the signature is valid @@ -4207,11 +4287,13 @@ TALER_wallet_withdraw_sign ( #define TALER_wallet_withdraw_sign_without_age(amount, \ fee, \ h_planchets, \ + blinding_seed, \ reserve_priv, \ reserve_sig) \ TALER_wallet_withdraw_sign ((amount), \ (fee), \ (h_planchets), \ + (blinding_seed), \ NULL, \ 0, \ (reserve_priv), \ @@ -4224,6 +4306,7 @@ TALER_wallet_withdraw_sign ( * @param amount total amount to withdraw, excluding fees * @param fee total amount of fees * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param blinding_seed blinding_seed used in a prior call to /blinding-prepare, if any of the denomination is of cipher type Clause-Schnorr, might be NULL * @param mask age mask to apply, or NULL, if not applicable. * @param max_age maximum age (in years) to commit to. Must be 0 if age restriction does not apply * @param reserve_pub public key of the reserve @@ -4235,6 +4318,7 @@ TALER_wallet_withdraw_verify ( const struct TALER_Amount *amount, const struct TALER_Amount *fee, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePublicKeyP *reserve_pub, @@ -4248,6 +4332,7 @@ TALER_wallet_withdraw_verify ( * @param amount total amount to withdraw, excluding fees * @param fee total amount of fees * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param blinding_seed seed used in a prior call to /blinding-prepare, if any of the denomination is of cipher type Clause-Schnorr, might be NULL * @param reserve_pub public key of the reserve * @param reserve_sig resulting signature * @return #GNUNET_OK if the signature is valid @@ -4255,11 +4340,13 @@ TALER_wallet_withdraw_verify ( #define TALER_wallet_withdraw_verify_without_age(amount, \ fee, \ h_planchets, \ + blinding_seed, \ reserve_pub, \ reserve_sig) \ TALER_wallet_withdraw_verify ((amount), \ (fee), \ (h_planchets), \ + (blinding_seed), \ NULL, \ 0, \ (reserve_pub), \ diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h @@ -2188,6 +2188,18 @@ struct TALER_EXCHANGE_ReserveHistoryEntry struct TALER_HashBlindedPlanchetsP h_planchets; /** + * True, if no blinding_seed was provided. The value of + * @e blinding_seed is then zero. + */ + bool no_blinding_seed; + + /** + * In case of denominations of cipher type Clause-Schnorr, the + * seed for the prior call to /blinding-prepare + */ + struct TALER_BlindingMasterSeedP blinding_seed; + + /** * Fee that was charged for the withdrawal. */ struct TALER_Amount fee; @@ -2692,8 +2704,10 @@ typedef void * * @param curl_ctx The curl context * @param exchange_url The base url of the exchange + * @param seed The blinding master seed for the batch + * @param for_melt True if this preparation of for melting/refresh; for withdraw otherwise * @param num Number of elements in @e nonces and @e denoms_h - * @param nonces The @e num nonces for Clause-Schnorr + * @param indices Array of @e num indices of the coins as they will appear in the subsequent operation * @param denoms_h The corresponding @e num denomination public keys * @param callback A callback for the result, maybe NULL * @param callback_cls A closure for @e res_cb, maybe NULL @@ -2703,9 +2717,11 @@ struct TALER_EXCHANGE_BlindingPrepareHandle * TALER_EXCHANGE_blinding_prepare ( struct GNUNET_CURL_Context *curl_ctx, const char *exchange_url, + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, size_t num, - const union GNUNET_CRYPTO_BlindSessionNonce *nonces, - const struct TALER_DenominationHashP *denoms_h, + const uint32_t indices[static num], + const struct TALER_DenominationHashP denoms_h[static num], TALER_EXCHANGE_BlindingPrepareCallback callback, void *callback_cls); @@ -2730,6 +2746,11 @@ TALER_EXCHANGE_blinding_prepare_cancel ( struct TALER_EXCHANGE_WithdrawCoinPrivateDetails { /** + * The seed that the various secrets were derived from + */ + struct TALER_PlanchetMasterSecretP secret; + + /** * Private key of the coin. */ struct TALER_CoinSpendPrivateKeyP coin_priv; @@ -2859,6 +2880,12 @@ struct TALER_EXCHANGE_WithdrawResponse * Key used by the exchange to sign the response. */ struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * The #TALER_CNC_KAPPA seeds that were used per batch of coin candidates + */ + struct TALER_KappaWithdrawMasterSeedP kappa_seed; + } created; /** @@ -2896,9 +2923,9 @@ typedef void * @param keys The denomination keys from the exchange * @param reserve_priv The private key to the reserve * @param num_coins Number of coins to withdraw in a batch - * @param opaque_max_age The age to commit to opaquely. If not zero, the denominations MUST support age restriction. * @param denoms_pub Array of @e num_coins of denominations of the coins to withdraw * @param seed seed from which @e num_coins secrets for each coin are derived from + * @param opaque_max_age The age to commit to opaquely. If not zero, the denominations MUST support age restriction. * @param callback A callback for the result, maybe NULL * @param callback_cls A closure for @e res_cb, maybe NULL * @return a handle for this request; NULL if the argument was invalid. @@ -2911,9 +2938,9 @@ TALER_EXCHANGE_withdraw ( const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, size_t num_coins, - uint8_t opaque_max_age, const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], const struct TALER_WithdrawMasterSeedP *seed, + uint8_t opaque_max_age, TALER_EXCHANGE_WithdrawCallback callback, void *callback_cls); @@ -2934,10 +2961,10 @@ TALER_EXCHANGE_withdraw ( * @param exchange_url The base url of the exchange * @param keys The denomination keys from the exchange * @param reserve_priv The private key to the reserve - * @param max_age maximum age to commit to * @param num_coins Number of coins to withdraw in a batch * @param denoms_pub Array of @e num_coins of denominations of the coins to withdraw, MUST support age restriction - * @param seeds tuple of TALER_CNC_KAPPA seeds from which all @e num_coins coin candidates are derived from + * @param seed seed from which all @e num_coins coin candidates and other secrets are derived from + * @param max_age maximum age to (provably) commit to * @param callback A callback for the result, maybe NULL * @param callback_cls A closure for @e res_cb, maybe NULL * @return a handle for this request; NULL if the argument was invalid. @@ -2949,10 +2976,10 @@ TALER_EXCHANGE_withdraw_with_age_proof ( struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, - uint8_t max_age, size_t num_coins, const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], - const struct TALER_KappaWithdrawMasterSeedP *seeds, + const struct TALER_WithdrawMasterSeedP *seed, + uint8_t max_age, TALER_EXCHANGE_WithdrawCallback callback, void *callback_cls); @@ -2968,6 +2995,49 @@ TALER_EXCHANGE_withdraw_cancel ( struct TALER_EXCHANGE_WithdrawHandle *awh); +/** + * These functions are made available for testing purposes. + * They SHOULD NOT be used in real applications! + */ +#ifdef TALER_TESTING_EXPORTS_DANGEROUS +/** + * Same as @e TALER_EXCHANGE_withdraw, + * but takes an additional blinding_seed as parameter. + */ +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw_extra_blinding_seed ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_coins, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_WithdrawMasterSeedP *seed, + const struct TALER_BlindingMasterSeedP *blinding_seed, + uint8_t opaque_max_age, + TALER_EXCHANGE_WithdrawCallback callback, + void *callback_cls); + +/** + * Same as @e TALER_EXCHANGE_withdraw_age_proof, + * but takes an additional blinding_seed as parameter. + */ +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw_with_age_proof_extra_blinding_seed ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_coins, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_WithdrawMasterSeedP *seed, + const struct TALER_BlindingMasterSeedP *blinding_seed, + uint8_t max_age, + TALER_EXCHANGE_WithdrawCallback res_cb, + void *res_cb_cls); + +#endif + /**++++++ withdraw with pre-blinded planchets ***************************/ @@ -3101,11 +3171,12 @@ struct TALER_EXCHANGE_WithdrawBlindedHandle; * same arguments in case of failures. * * @param curl_ctx The curl context to use - * @param exchange_url The base-URL of the exchange * @param keys The /keys material from the exchange + * @param exchange_url The base-URL of the exchange + * @param reserve_priv private key of the reserve to withdraw from + * @param blinding_seed seed used for blinding of CS denominations, might be NULL * @param num_input number of entries in the @a blinded_input array * @param blinded_input array of planchet details of the planchet to withdraw - * @param reserve_priv private key of the reserve to withdraw from * @param res_cb the callback to call when the final result for this request is available * @param res_cb_cls closure for @a res_cb * @return NULL @@ -3118,6 +3189,7 @@ TALER_EXCHANGE_withdraw_blinded ( struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_BlindingMasterSeedP *blinding_seed, size_t num_input, const struct TALER_EXCHANGE_WithdrawBlindedCoinInput blinded_input[static num_input], @@ -3136,12 +3208,13 @@ TALER_EXCHANGE_withdraw_blinded ( * to all but one index all of the coin secrets to the intermediary. * * @param curl_ctx The curl context to use - * @param exchange_url The base-URL of the exchange * @param keys The /keys material from the exchange + * @param exchange_url The base-URL of the exchange + * @param reserve_priv private key of the reserve to withdraw from + * @param blinding_seed seed used for blinding of CS denominations, might be NULL * @param max_age the maximum age to commit to * @param num_coins number of entries in the @a blinded_input array * @param blinded_input array of planchet details of the planchet to withdraw - * @param reserve_priv private key of the reserve to withdraw from * @param res_cb the callback to call when the final result for this request is available * @param res_cb_cls closure for @a res_cb * @return NULL @@ -3155,6 +3228,7 @@ TALER_EXCHANGE_withdraw_blinded_with_age_proof ( struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_BlindingMasterSeedP *blinding_seed, uint8_t max_age, unsigned int num_coins, const struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h @@ -848,8 +848,12 @@ struct TALER_EXCHANGEDB_TableData struct TALER_ReserveSignatureP reserve_sig; uint64_t num_coins; uint64_t *denominations_serials; - struct TALER_BlindedCoinHashP *h_blind_evs; - struct TALER_BlindedDenominationSignature denom_sigs; + struct TALER_BlindedCoinHashP *h_coin_evs; + struct TALER_BlindedDenominationSignature *denom_sigs; + bool no_blinding_seed; + struct TALER_BlindingMasterSeedP blinding_seed; + uint64_t num_cs_r_pubs; + struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_pubs; } withdraw; } details; @@ -1320,9 +1324,37 @@ struct TALER_EXCHANGEDB_Withdraw uint64_t *denom_serials; /** + * Number of elements in @e cs_r_pubs. + * Only non-zero IF @e age_proof_required is true AND any of the denomination + * has a cipher of type CS. + */ + size_t num_cs_r_pubs; + + /** + * Array @e num_r_pubs of public R-values for CS that were generated from the + * @e blinding_seed, a coin's index and the denomination's private key during the + * the /withdraw request, to ensure idempotency in case of expiration of a denomination. + * NULL if @e num_r_pub is 0 (or @e age_proof_required is false). + */ + struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_pubs; + + /** + * True, if @e blinding_seed is empty. This is the case when + * there no denomination with cipher type CS was used. + */ + bool no_blinding_seed; + + /** + * The master secret for the blinding, needed for CS-signatures, for this withdraw. + * Only valid if @e no_blinding_seed is false. + * This value must be unique across _all_ entries in the withdraw table. + */ + struct TALER_BlindingMasterSeedP blinding_seed; + + /** * [out]-Array of @a num_coins hashes of the public keys of the denominations - * identified by @e denom_serials. This field is set when calling - * get_withdraw + * identified by @e denom_serials. This field is only set when calling + * get_reserve_history(). */ struct TALER_DenominationHashP *denom_pub_hashes; }; @@ -2538,6 +2570,26 @@ struct TALER_EXCHANGEDB_Refresh_v26 uint32_t noreveal_index; /** + * Number of elements in @e r_pubs. + */ + size_t num_r_pubs; + + /** + * Array @e num_r_pubs of public R-values for CS that were generated from the + * @e blinding_seed, a coin's index and the denomination's private key during the + * the /melt request, to ensure idempotency in case of expiration of a denomination. + * NULL if @e num_r_pub is 0. + */ + struct GNUNET_CRYPTO_CSPublicRPairP *r_pubs; + + /** + * The master secret for the blinding, needed for CS, for this refresh, if applicable. + * Only non-zero when @e num_r_pubs is not 0. + * Note that this value must be unique across _all_ entries in the refresh table. + */ + struct TALER_BlindingMasterSeedP blinding_seed; + + /** * [out]-Array of @a num_coins hashes of the public keys of the denominations * identified by @e denom_serials. This field is set when calling * get_melt_v26 @@ -3367,6 +3419,7 @@ typedef void * @param h_blind_evs array @e num_evs of blinded hashes of the coin's public keys * @param denom_serials array @e num_evs of serial ID's of denominations in our DB * @param h_planchets running hash over all hashes of blinded planchets in the original withdraw request + * @param blinding_seed the blinding seed for CS denominations that was provided during withdraw; might be NULL * @param age_proof_required true if the withdraw request required an age proof. * @param max_age if @e age_proof_required is true, the maximum age that was set on the coins. * @param noreveal_index if @e age_proof_required is true, the index that was returned by the exchange for the reveal phase. @@ -3384,6 +3437,7 @@ typedef enum GNUNET_GenericReturnValue const struct TALER_BlindedCoinHashP *h_blind_evs, const uint64_t *denom_serials, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, bool age_proof_required, uint8_t max_age, uint8_t noreveal_index, @@ -4354,25 +4408,28 @@ struct TALER_EXCHANGEDB_Plugin * details. * * @param cls the `struct PostgresClosure` with the plugin-specific state - * @param commitment corresponding commitment for the age-withdraw + * @param withdraw the withdraw data * @param[out] balance_ok set to true if the balance was sufficient * @param[out] reserve_balance set to original balance of the reserve * @param[out] age_ok set to true if age requirements were met * @param[out] allowed_maximum_age if @e age_ok is FALSE, this is set to the allowed maximum age * @param[out] reserve_birthday if @e age_ok is FALSE, this is set to the reserve's birthday + * @param[out] conflict set to true if an entry already exists for the given h_planchets or blinding_seed + * @param[out] nonce_resue set to true if the blinding_seed has been found in the table for a different withdraw * @return 0 if no reserve was found, 1 if a reserve was found, else the query execution status */ enum GNUNET_DB_QueryStatus (*do_withdraw)( void *cls, - const struct TALER_EXCHANGEDB_Withdraw *commitment, + const struct TALER_EXCHANGEDB_Withdraw *withdraw, struct GNUNET_TIME_Timestamp now, bool *balance_ok, struct TALER_Amount *reserve_balance, bool *age_ok, uint16_t *allowed_maximum_age, uint32_t *reserve_birthday, - bool *conflict); + bool *conflict, + bool *nonce_reuse); /** * Retrieve the details to a policy given by its hash_code @@ -4479,8 +4536,6 @@ struct TALER_EXCHANGEDB_Plugin * @param cls the plugin-specific state * @param[in,out] refresh refresh operation details; the noreveal_index * is set in case the coin was already melted before - * @param rms client-contributed input for CS denominations that must be checked - * for idempotency, or NULL for non-CS withdrawals * @param timestamp the timestamp of this melt operation, helpful for the coin history. * @param[in,out] zombie_required true if the melt must only succeed * if the coin is a zombie, set to false if the requirement was satisfied @@ -4494,7 +4549,6 @@ struct TALER_EXCHANGEDB_Plugin (*do_melt_v26)( void *cls, struct TALER_EXCHANGEDB_Refresh_v26 *refresh, - const struct TALER_RefreshMasterSecretP *rms, const struct GNUNET_TIME_Timestamp *timestamp, bool *conflict, bool *zombie_required, diff --git a/src/include/taler_pq_lib.h b/src/include/taler_pq_lib.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2022 Taler Systems SA + Copyright (C) 2014-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -33,7 +33,7 @@ /** * API version. Bump on every change. */ -#define TALER_PQ_VERSION 0x09040000 +#define TALER_PQ_VERSION 0x0a040000 /** * Generate query parameter (as record tuple) for an amount, consisting @@ -225,6 +225,19 @@ TALER_PQ_query_param_array_amount_with_currency ( const struct TALER_Amount *amounts, struct GNUNET_PQ_Context *db); +/** + * Generate query parameter for an array of public Clause-Schnorr R-values + * + * @param num of elements in @e cs_r_pubs + * @param cs_r_pubs continuous array of public R-values pairs + * @param db context fo db-connection, needed for OID-lookup + */ +struct GNUNET_PQ_QueryParam +TALER_PQ_query_param_array_cs_r_pub ( + size_t num, + const struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_pubs, + struct GNUNET_PQ_Context *db); + /** * Generate query parameter for a blind sign public key of variable size. @@ -246,6 +259,9 @@ TALER_PQ_query_param_blind_sign_priv ( const struct GNUNET_CRYPTO_BlindSignPrivateKey *private_key); +/* ================ Result handler ==============================*/ + + /** * Currency amount expected, from a record-field of (DB) * taler_amount_with_currency type. The currency must be stored in the @@ -438,6 +454,22 @@ TALER_PQ_result_spec_array_amount ( struct TALER_Amount **amounts); +/** + * Array of public R-values for Clause-Schnorr + * + * @param db context of the database connection + * @param name name of the field in the table + * @param[out] num number of elements in @e cs_r_pubs + * @param[out] cs_r_pubs where to store the result + * @return array entry for the result specification to use + */ +struct GNUNET_PQ_ResultSpec +TALER_PQ_result_spec_array_cs_r_pub ( + struct GNUNET_PQ_Context *db, + const char *name, + size_t *num, + struct GNUNET_CRYPTO_CSPublicRPairP **cs_r_pubs); + #endif /* TALER_PQ_LIB_H_ */ /* end of include/taler_pq_lib.h */ diff --git a/src/include/taler_testing_lib.h b/src/include/taler_testing_lib.h @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2018-2024 Taler Systems SA + (C) 2018-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as @@ -24,6 +24,7 @@ * This library calls abort() if it runs out of memory. Be aware of these limitations. * @author Christian Grothoff <christian@grothoff.org> * @author Marcello Stanisci + * @author Özgür Kesim */ #ifndef TALER_TESTING_LIB_H #define TALER_TESTING_LIB_H @@ -34,6 +35,10 @@ #include "taler_json_lib.h" #include "taler_auditor_service.h" #include "taler_bank_service.h" +/** + * To get access to testing relevant API's + */ +#define TALER_TESTING_EXPORTS_DANGEROUS 1 #include "taler_exchange_service.h" #include "taler_fakebank_lib.h" @@ -2769,6 +2774,7 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, op (account_pub, const union TALER_AccountPublicKeyP) \ op (planchet_secret, const struct TALER_PlanchetMasterSecretP) \ op (withdraw_seed, const struct TALER_WithdrawMasterSeedP) \ + op (blinding_seed, const struct TALER_BlindingMasterSeedP) \ op (withdraw_commitment, const struct TALER_HashBlindedPlanchetsP) \ op (kappa_seed, const struct TALER_KappaWithdrawMasterSeedP) \ op (refresh_secret, const struct TALER_RefreshMasterSecretP) \ @@ -2821,7 +2827,6 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, op (h_age_commitment, const struct TALER_AgeCommitmentHash) \ op (coin_history, const struct TALER_EXCHANGE_CoinHistoryEntry) \ op (planchet_secrets, const struct TALER_PlanchetMasterSecretP) \ - op (withdraw_seeds, const struct TALER_WithdrawMasterSeedP) \ op (kappa_seeds, const struct TALER_KappaWithdrawMasterSeedP) \ op (exchange_wd_value, const struct TALER_ExchangeWithdrawValues) \ op (coin_priv, const struct TALER_CoinSpendPrivateKeyP) \ diff --git a/src/lib/exchange_api_blinding_prepare.c b/src/lib/exchange_api_blinding_prepare.c @@ -49,14 +49,28 @@ struct TALER_EXCHANGE_BlindingPrepareHandle size_t num; /** - * The @a num nonces for Clause-Schnorr + * True, if this operation is for melting (or withdraw otherwise). */ - const union GNUNET_CRYPTO_BlindSessionNonce *nonces; + bool for_melt; + + /** + * The seed for the batch of nonces. + */ + const struct TALER_BlindingMasterSeedP *seed; + + /** + * The array of indices of the coins, as they will appear in the + * subsequent operation (melt or withdraw), from which the nonces + * are generated. + */ + const uint32_t *indices; /** * The corresponding @a num denomination public keys */ const struct TALER_DenominationHashP *denoms_h; + + /** * The url for this request. */ @@ -301,9 +315,11 @@ struct TALER_EXCHANGE_BlindingPrepareHandle * TALER_EXCHANGE_blinding_prepare ( struct GNUNET_CURL_Context *curl_ctx, const char *exchange_url, + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, size_t num, - const union GNUNET_CRYPTO_BlindSessionNonce *nonces, - const struct TALER_DenominationHashP *denoms_h, + const uint32_t indices[static num], + const struct TALER_DenominationHashP denoms_h[static num], TALER_EXCHANGE_BlindingPrepareCallback callback, void *callback_cls) { @@ -311,7 +327,7 @@ TALER_EXCHANGE_blinding_prepare ( bph = GNUNET_new (struct TALER_EXCHANGE_BlindingPrepareHandle); bph->num = num; - bph->nonces = nonces; + bph->indices = indices; bph->denoms_h = denoms_h; bph->callback = callback; bph->callback_cls = callback_cls; @@ -324,22 +340,39 @@ TALER_EXCHANGE_blinding_prepare ( GNUNET_free (bph); return NULL; } - { CURL *eh; + json_t *j_nks; json_t *j_request = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("cipher", "CS"), - TALER_JSON_pack_array_of_data ("nonces", - num, - nonces, - sizeof(nonces[0])), - TALER_JSON_pack_array_of_data ("denoms_h", - num, - denoms_h, - sizeof(denoms_h[0]))); + GNUNET_JSON_pack_string ("operation", + for_melt ? "refresh" : "withdraw"), + GNUNET_JSON_pack_data_auto ("seed", + seed)); GNUNET_assert (NULL != j_request); + j_nks = json_array (); + GNUNET_assert (NULL != j_nks); + + for (size_t i = 0; i<num; i++) + { + json_t *j_entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("coin_offset", + indices[i]), + GNUNET_JSON_pack_data_auto ("denom_pub_hash", + &denoms_h[i])); + + GNUNET_assert (NULL != j_entry); + GNUNET_assert (0 == + json_array_append_new (j_nks, + j_entry)); + } + GNUNET_assert (0 == + json_object_set_new (j_request, + "nks", + j_nks)); + eh = TALER_EXCHANGE_curl_easy_get_ (bph->url); if ( (NULL == eh) || (GNUNET_OK != diff --git a/src/lib/exchange_api_reserves_history.c b/src/lib/exchange_api_reserves_history.c @@ -207,10 +207,12 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, uint8_t noreveal_index = 0; struct TALER_HashBlindedPlanchetsP h_planchets; struct TALER_ReserveSignatureP reserve_sig; + struct TALER_BlindingMasterSeedP blinding_seed; const json_t *j_h_coin_evs; const json_t *j_denom_pub_hashes; bool no_max_age; bool no_noreveal_index; + bool no_blinding_seed; struct GNUNET_JSON_Specification withdraw_spec[] = { GNUNET_JSON_spec_fixed_auto ("reserve_sig", &reserve_sig), @@ -231,6 +233,10 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, &max_age), &no_max_age), GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("blinding_seed", + &blinding_seed), + &no_blinding_seed), + GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint8 ("noreveal_index", &noreveal_index), &no_noreveal_index), @@ -279,6 +285,7 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, &amount_without_fee, &withdraw_fee, &h_planchets, + no_blinding_seed ? NULL : &blinding_seed, no_max_age ? NULL : &uc->keys->age_mask, no_max_age ? 0 : max_age, uc->reserve_pub, @@ -296,6 +303,9 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, rh->details.withdraw.max_age = max_age; rh->details.withdraw.h_planchets = h_planchets; rh->details.withdraw.noreveal_index = noreveal_index; + rh->details.withdraw.no_blinding_seed = no_blinding_seed; + if (! no_blinding_seed) + rh->details.withdraw.blinding_seed = blinding_seed; #pragma message "finish parsing and verification of withdraw history entry" diff --git a/src/lib/exchange_api_withdraw.c b/src/lib/exchange_api_withdraw.c @@ -45,11 +45,6 @@ struct CoinCandidate { /** - * Master key material for the coin candidates. - */ - struct TALER_PlanchetMasterSecretP secret; - - /** * The details derived form the master secrets */ struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details; @@ -98,12 +93,25 @@ struct BlindingPrepareClosure * step after /blinding-prepare */ const struct TALER_DenominationPublicKey *denom_pub; + + /** + * The index into the array of returned values from the call to + * /blinding-prepare that are to be used for this coin. + */ + size_t cs_idx; + } *coins; /** - * Array @a num of original nonces from the request, - * needs to be freed. - */ + * Number of seeds requested. This may differ from @e num_prepare_coins + * in case of a withdraw with required age proof, in which case + * @e num_prepare_coins = TALER_CNC_KAPPA * @e num_seeds + */ + size_t num_nonces; + + /** + * Array of @e num_nonces calculated nonces. + */ union GNUNET_CRYPTO_BlindSessionNonce *nonces; /** @@ -177,6 +185,13 @@ struct TALER_EXCHANGE_WithdrawBlindedHandle struct TALER_HashBlindedPlanchetsP h_planchets; /** + * Seed used for the derival of blinding factors for denominations + * with Clause-Schnorr cipher. We derive this from the master seed + * for the withdraw, but independent from the other planchet seeds. + */ + const struct TALER_BlindingMasterSeedP *blinding_seed; + + /** * Total amount requested (without fee). */ struct TALER_Amount amount; @@ -206,7 +221,6 @@ struct TALER_EXCHANGE_WithdrawBlindedHandle */ uint8_t max_age; - /** * Length of the either the @e blinded.input or * the @e blinded.with_age_proof_input array, @@ -275,6 +289,31 @@ struct TALER_EXCHANGE_WithdrawHandle const char *exchange_url; /** + * Seed to derive of all seeds for the coins. + */ + struct TALER_WithdrawMasterSeedP seed; + + /** + * If @e with_age_proof is true, the derived TALER_CNC_KAPPA many + * seeds for candidate batches. + */ + struct TALER_KappaWithdrawMasterSeedP kappa_seed; + + /** + * True if @e blinding_seed is filled, that is, if + * any of the denominations is of cipher type CS + */ + bool has_blinding_seed; + + /** + * Seed used for the derivation of blinding factors for denominations + * with Clause-Schnorr cipher. We derive this from the master seed + * for the withdraw, but independent from the other planchet seeds. + * Only valid when @e has_blinding_seed is true; + */ + struct TALER_BlindingMasterSeedP blinding_seed; + + /** * Reserve private key. */ const struct TALER_ReservePrivateKeyP *reserve_priv; @@ -688,6 +727,11 @@ perform_withdraw_protocol ( TALER_amount_add (&wbh->fee, &wbh->fee, &dpub->fees.withdraw)); + + if (GNUNET_CRYPTO_BSA_CS == + dpub->key.bsign_pub_key->cipher) + GNUNET_assert (NULL != wbh->blinding_seed); + } if (wbh->with_age_proof || wbh->max_age > 0) @@ -808,6 +852,7 @@ perform_withdraw_protocol ( &wbh->amount, &wbh->fee, &wbh->h_planchets, + wbh->blinding_seed, wbh->with_age_proof ? &wbh->age_mask: NULL, wbh->with_age_proof ? wbh->max_age : 0, wbh->reserve_priv, @@ -833,6 +878,18 @@ perform_withdraw_protocol ( &wbh->reserve_sig)); FAIL_IF (NULL == j_request_body); + if (NULL != wbh->blinding_seed) + { + json_t *j_seed = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("blinding_seed", + wbh->blinding_seed)); + GNUNET_assert (NULL != j_seed); + GNUNET_assert (0 == + json_object_update_new ( + j_request_body, + j_seed)); + } + curlh = TALER_EXCHANGE_curl_easy_get_ (wbh->request_url); FAIL_IF (NULL == curlh); FAIL_IF (GNUNET_OK != @@ -918,7 +975,7 @@ copy_results ( &wbr->details.ok.blinded_denom_sigs[n]; struct CoinData *cd = &wh->coin_data[n]; struct TALER_EXCHANGE_WithdrawCoinPrivateDetails *coin = &details[n]; - struct TALER_FreshCoin fc; + struct TALER_FreshCoin fresh_coin; *coin = wh->coin_data[n].candidates[0].details; coin->planchet = wh->coin_data[n].planchet_details[0]; @@ -934,15 +991,14 @@ copy_results ( &coin->h_age_commitment, &coin->h_coin_pub, &coin->alg_values, - &fc)) + &fresh_coin)) { resp.hr.http_status = 0; resp.hr.ec = TALER_EC_EXCHANGE_WITHDRAW_UNBLIND_FAILURE; GNUNET_break_op (0); break; } - coin->denom_sig = fc.sig; - + coin->denom_sig = fresh_coin.sig; } break; } @@ -1009,6 +1065,7 @@ copy_results_with_age_proof ( GNUNET_assert (wh->num_coins == wbr->details.created.num_coins); resp.details.created = wbr->details.created; resp.details.created.coin_details = details; + resp.details.created.kappa_seed = wh->kappa_seed; memset (details, 0, sizeof(details)); @@ -1067,6 +1124,7 @@ call_withdraw_blinded ( wh->keys, wh->exchange_url, wh->reserve_priv, + wh->has_blinding_seed ? &wh->blinding_seed : NULL, wh->num_coins, input, &copy_results, @@ -1096,6 +1154,7 @@ call_withdraw_blinded ( wh->keys, wh->exchange_url, wh->reserve_priv, + wh->has_blinding_seed ? &wh->blinding_seed : NULL, wh->max_age, wh->num_coins, ari, @@ -1131,12 +1190,13 @@ blinding_prepare_done ( bool success = false; size_t num = bpr->details.ok.num; GNUNET_assert (0 != num); - GNUNET_assert (num == bpcls->num_prepare_coins); + GNUNET_assert (num == bpcls->num_nonces); - for (size_t i = 0; i < num; i++) + for (size_t i = 0; i < bpcls->num_prepare_coins; i++) { struct TALER_PlanchetDetail *planchet = bpcls->coins[i].planchet; struct CoinCandidate *can = bpcls->coins[i].candidate; + size_t cs_idx = bpcls->coins[i].cs_idx; GNUNET_assert (NULL != can); GNUNET_assert (NULL != planchet); @@ -1145,18 +1205,18 @@ blinding_prepare_done ( /* Complete the initialization of the coin with CS denomination */ TALER_denom_ewv_copy ( &can->details.alg_values, - &bpr->details.ok.alg_values[i]); + &bpr->details.ok.alg_values[cs_idx]); GNUNET_assert (GNUNET_CRYPTO_BSA_CS == can->details.alg_values.blinding_inputs->cipher); TALER_planchet_setup_coin_priv ( - &can->secret, + &can->details.secret, &can->details.alg_values, &can->details.coin_priv); TALER_planchet_blinding_secret_create ( - &can->secret, + &can->details.secret, &can->details.alg_values, &can->details.blinding_key); @@ -1167,7 +1227,7 @@ blinding_prepare_done ( bpcls->coins[i].denom_pub, &can->details.alg_values, &can->details.blinding_key, - &bpcls->nonces[i], + &bpcls->nonces[cs_idx], &can->details.coin_priv, &can->details.h_age_commitment, &can->details.h_coin_pub, @@ -1222,8 +1282,8 @@ blinding_prepare_done ( * @param num_coins Number of coins to withdraw * @param max_age The maximum age to commit to * @param denoms_pub Array @e num_coins of denominations - * @param tuples tuple of seeds to derive @e num_coins secrets for age-restricted coins, might be NULL - * @param secrets Array of @e num_coins planchet secrets for the coins, might be NULL + * @param seed master seed from which to derive @e num_coins secrets and blinding, if @e blinding_seed is NULL + * @param blinding_seed master seed for the blinding. Might be NULL, in which case the blinding_seed is derived from @e seed * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure */ static enum GNUNET_GenericReturnValue @@ -1232,11 +1292,11 @@ prepare_coins ( size_t num_coins, uint8_t max_age, const struct TALER_EXCHANGE_DenomPublicKey *denoms_pub, - const struct TALER_KappaWithdrawMasterSeedP *tuples, - const struct TALER_PlanchetMasterSecretP *secrets) + const struct TALER_WithdrawMasterSeedP *seed, + const struct TALER_BlindingMasterSeedP *blinding_seed) { size_t cs_num = 0; - struct BlindingPrepareClosure *cs_closure = NULL; + struct BlindingPrepareClosure *cs_closure; uint8_t kappa; #define FAIL_IF(cond) \ @@ -1249,9 +1309,6 @@ prepare_coins ( } \ } while (0) - GNUNET_assert ( - ((NULL != tuples) && (NULL == secrets)) || - ((NULL == tuples) && (NULL != secrets))); GNUNET_assert (0 < num_coins); wh->num_coins = num_coins; @@ -1272,7 +1329,6 @@ prepare_coins ( if (wh->with_age_proof) { kappa = TALER_CNC_KAPPA; - cs_num *= TALER_CNC_KAPPA; } else { @@ -1280,21 +1336,31 @@ prepare_coins ( } { - size_t cs_cnt = 0; - struct TALER_DenominationHashP cs_denoms_h[cs_num > 0 ? cs_num : 1]; - struct TALER_PlanchetMasterSecretP kappa_secrets[kappa][num_coins]; + struct TALER_PlanchetMasterSecretP secrets[kappa][num_coins]; + struct TALER_DenominationHashP cs_denoms_h[(cs_num > 0) ? cs_num : 1]; + uint32_t cs_indices[(cs_num > 0)? cs_num : 1]; + size_t cs_denom_idx; + size_t cs_coin_idx; if (wh->with_age_proof) { - GNUNET_assert (NULL != tuples); + TALER_expand_seed_to_kappa_seed (seed, + &wh->kappa_seed); for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) { TALER_expand_withdraw_secrets ( num_coins, - &tuples->tuple[k], - kappa_secrets[k]); + &wh->kappa_seed.tuple[k], + secrets[k]); } } + else + { + TALER_expand_withdraw_secrets ( + num_coins, + seed, + secrets[0]); + } if (0 < cs_num) { @@ -1302,17 +1368,25 @@ prepare_coins ( 0, sizeof(cs_denoms_h)); cs_closure = GNUNET_new (struct BlindingPrepareClosure); - cs_closure->num_prepare_coins = cs_num; cs_closure->withdraw_handle = wh; + cs_closure->num_prepare_coins = cs_num * kappa; + GNUNET_assert ((1 == kappa) || (cs_num * kappa > cs_num)); cs_closure->coins = - GNUNET_new_array (cs_num, + GNUNET_new_array (cs_closure->num_prepare_coins, struct BlindingPrepareCoinData); + cs_closure->num_nonces = cs_num; cs_closure->nonces = - GNUNET_new_array (cs_num, + GNUNET_new_array (cs_closure->num_nonces, union GNUNET_CRYPTO_BlindSessionNonce); + cs_coin_idx = 0; + cs_denom_idx = 0; + } + else + { + cs_closure = NULL; } - for (size_t i = 0; i < wh->num_coins; i++) + for (uint32_t i = 0; i < wh->num_coins; i++) { struct CoinData *cd = &wh->coin_data[i]; @@ -1325,6 +1399,18 @@ prepare_coins ( TALER_denom_pub_copy (&cd->denom_pub.key, &denoms_pub[i].key); + /* Mark the indices of the coins which are of type Clause-Schnorr + * and add their denomination public key hash to the list. + */ + if (GNUNET_CRYPTO_BSA_CS == + cd->denom_pub.key.bsign_pub_key->cipher) + { + GNUNET_assert (cs_denom_idx<cs_num); + cs_indices[cs_denom_idx] = i; + cs_denoms_h[cs_denom_idx] = cd->denom_pub.h_key; + cs_denom_idx++; + } + /* * Note that we "loop" here either only once (if with_age_proof is false), * or TALER_CNC_KAPPA times. @@ -1334,11 +1420,7 @@ prepare_coins ( struct CoinCandidate *can = &cd->candidates[k]; struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k]; - if (wh->with_age_proof) - can->secret = kappa_secrets[k][i]; - else - can->secret = secrets[i]; - + can->details.secret = secrets[k][i]; /* * The age restriction needs to be set on a coin if the denomination * support age restriction. Note that his is regardless of weither @@ -1349,7 +1431,7 @@ prepare_coins ( /* Derive the age restriction from the given secret and * the maximum age */ TALER_age_restriction_from_secret ( - &can->secret, + &can->details.secret, &wh->age_mask, wh->max_age, &can->details.age_commitment_proof); @@ -1364,10 +1446,10 @@ prepare_coins ( case GNUNET_CRYPTO_BSA_RSA: TALER_denom_ewv_copy (&can->details.alg_values, TALER_denom_ewv_rsa_singleton ()); - TALER_planchet_setup_coin_priv (&can->secret, + TALER_planchet_setup_coin_priv (&can->details.secret, &can->details.alg_values, &can->details.coin_priv); - TALER_planchet_blinding_secret_create (&can->secret, + TALER_planchet_blinding_secret_create (&can->details.secret, &can->details.alg_values, &can->details.blinding_key); FAIL_IF (GNUNET_OK != @@ -1388,20 +1470,14 @@ prepare_coins ( case GNUNET_CRYPTO_BSA_CS: { /** - * Prepare the nonce and save the index and the denomination for the callback - * after the call to blinding-prepare - */ - - cs_closure->coins[cs_cnt].candidate = can; - cs_closure->coins[cs_cnt].planchet = planchet; - cs_closure->coins[cs_cnt].denom_pub = &cd->denom_pub.key; - cs_denoms_h[cs_cnt] = cd->denom_pub.h_key; - - TALER_cs_withdraw_nonce_derive ( - &can->secret, - &cs_closure->nonces[cs_cnt].cs_nonce); - - cs_cnt++; + * Prepare the nonce and save the index and the denomination for the callback + * after the call to blinding-prepare + */ + cs_closure->coins[cs_coin_idx].candidate = can; + cs_closure->coins[cs_coin_idx].planchet = planchet; + cs_closure->coins[cs_coin_idx].denom_pub = &cd->denom_pub.key; + cs_closure->coins[cs_coin_idx].cs_idx = i; + cs_coin_idx++; break; } default: @@ -1412,12 +1488,33 @@ prepare_coins ( if (0 < cs_num) { + if (NULL != blinding_seed) + { + wh->blinding_seed = *blinding_seed; + } + else + { + TALER_cs_withdraw_seed_to_blinding_seed ( + seed, + &wh->blinding_seed); + } + wh->has_blinding_seed = true; + + TALER_cs_derive_blind_nonces_from_seed ( + &wh->blinding_seed, + false, /* not for melt */ + cs_num, + cs_indices, + cs_closure->nonces); + wh->blinding_prepare_handle = TALER_EXCHANGE_blinding_prepare ( wh->curl_ctx, wh->exchange_url, + &wh->blinding_seed, + false, /* not for melt */ cs_num, - cs_closure->nonces, + cs_indices, cs_denoms_h, &blinding_prepare_done, cs_closure); @@ -1474,25 +1571,20 @@ setup_withdraw_handle ( struct TALER_EXCHANGE_WithdrawHandle * -TALER_EXCHANGE_withdraw ( +TALER_EXCHANGE_withdraw_extra_blinding_seed ( struct GNUNET_CURL_Context *curl_ctx, struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, size_t num_coins, - uint8_t opaque_max_age, const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], const struct TALER_WithdrawMasterSeedP *seed, + const struct TALER_BlindingMasterSeedP *blinding_seed, + uint8_t opaque_max_age, TALER_EXCHANGE_WithdrawCallback res_cb, void *res_cb_cls) { struct TALER_EXCHANGE_WithdrawHandle *wh; - struct TALER_PlanchetMasterSecretP secrets[num_coins]; - - TALER_expand_withdraw_secrets ( - num_coins, - seed, - secrets); wh = setup_withdraw_handle (curl_ctx, keys, @@ -1508,8 +1600,8 @@ TALER_EXCHANGE_withdraw ( num_coins, opaque_max_age, denoms_pub, - NULL, - secrets)) + seed, + blinding_seed)) { GNUNET_free (wh); return NULL; @@ -1528,15 +1620,45 @@ TALER_EXCHANGE_withdraw ( struct TALER_EXCHANGE_WithdrawHandle * -TALER_EXCHANGE_withdraw_with_age_proof ( +TALER_EXCHANGE_withdraw ( struct GNUNET_CURL_Context *curl_ctx, struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, - uint8_t max_age, size_t num_coins, const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], - const struct TALER_KappaWithdrawMasterSeedP *seeds, + const struct TALER_WithdrawMasterSeedP *seed, + uint8_t opaque_max_age, + TALER_EXCHANGE_WithdrawCallback res_cb, + void *res_cb_cls) +{ + return TALER_EXCHANGE_withdraw_extra_blinding_seed ( + curl_ctx, + keys, + exchange_url, + reserve_priv, + num_coins, + denoms_pub, + seed, + NULL, + opaque_max_age, + res_cb, + res_cb_cls + ); +} + + +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw_with_age_proof_extra_blinding_seed ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_coins, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_WithdrawMasterSeedP *seed, + const struct TALER_BlindingMasterSeedP *blinding_seed, + uint8_t max_age, TALER_EXCHANGE_WithdrawCallback res_cb, void *res_cb_cls) { @@ -1557,8 +1679,8 @@ TALER_EXCHANGE_withdraw_with_age_proof ( num_coins, max_age, denoms_pub, - seeds, - NULL)) + seed, + blinding_seed)) { GNUNET_free (wh); return NULL; @@ -1576,6 +1698,34 @@ TALER_EXCHANGE_withdraw_with_age_proof ( } +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw_with_age_proof ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_coins, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_WithdrawMasterSeedP *seed, + uint8_t max_age, + TALER_EXCHANGE_WithdrawCallback res_cb, + void *res_cb_cls) +{ + return TALER_EXCHANGE_withdraw_with_age_proof_extra_blinding_seed ( + curl_ctx, + keys, + exchange_url, + reserve_priv, + num_coins, + denoms_pub, + seed, + NULL, + max_age, + res_cb, + res_cb_cls); +} + + void TALER_EXCHANGE_withdraw_cancel ( struct TALER_EXCHANGE_WithdrawHandle *wh) @@ -1660,6 +1810,7 @@ TALER_EXCHANGE_withdraw_blinded ( struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_BlindingMasterSeedP *blinding_seed, size_t num_input, const struct TALER_EXCHANGE_WithdrawBlindedCoinInput blinded_input[static num_input], @@ -1677,6 +1828,7 @@ TALER_EXCHANGE_withdraw_blinded ( wbh->with_age_proof = false; wbh->num_input = num_input; wbh->blinded.input = blinded_input; + wbh->blinding_seed = blinding_seed; perform_withdraw_protocol (wbh); return wbh; @@ -1689,6 +1841,7 @@ TALER_EXCHANGE_withdraw_blinded_with_age_proof ( struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, + const struct TALER_BlindingMasterSeedP *blinding_seed, uint8_t max_age, unsigned int num_input, const struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput @@ -1708,6 +1861,7 @@ TALER_EXCHANGE_withdraw_blinded_with_age_proof ( wbh->max_age = max_age; wbh->num_input = num_input; wbh->blinded.with_age_proof_input = blinded_input; + wbh->blinding_seed = blinding_seed; perform_withdraw_protocol (wbh); return wbh; diff --git a/src/pq/pq_common.h b/src/pq/pq_common.h @@ -45,12 +45,11 @@ enum TALER_PQ_ArrayType TALER_PQ_array_of_blinded_coin_hash, TALER_PQ_array_of_denom_hash, TALER_PQ_array_of_hash_code, - + TALER_PQ_array_of_cs_r_pub, /** * Amounts *without* currency. */ TALER_PQ_array_of_amount, - /** * Amounts *with* currency. */ diff --git a/src/pq/pq_query_helper.c b/src/pq/pq_query_helper.c @@ -949,6 +949,15 @@ qconv_array ( sizeof(struct GNUNET_HashCode)); break; } + case TALER_PQ_array_of_cs_r_pub: + { + const struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_pubs = data; + + GNUNET_memcpy (out, + &cs_r_pubs[i], + sizeof(struct GNUNET_CRYPTO_CSPublicRPairP)); + break; + } default: { GNUNET_assert (0); @@ -1162,4 +1171,28 @@ TALER_PQ_query_param_array_amount_with_currency ( } +struct GNUNET_PQ_QueryParam +TALER_PQ_query_param_array_cs_r_pub ( + size_t num, + const struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_pubs, + struct GNUNET_PQ_Context *db) +{ + Oid oid; + + GNUNET_assert (GNUNET_OK == + GNUNET_PQ_get_oid_by_name (db, + "bytea", + &oid)); + return query_param_array_generic ( + num, + true, + cs_r_pubs, + NULL, + sizeof(struct GNUNET_CRYPTO_CSPublicRPairP), + TALER_PQ_array_of_cs_r_pub, + oid, + db); +} + + /* end of pq/pq_query_helper.c */ diff --git a/src/pq/pq_result_helper.c b/src/pq/pq_result_helper.c @@ -1173,6 +1173,11 @@ extract_array_generic ( col_num = PQfnumber (result, fname); FAIL_IF (0 > col_num); + if (PQgetisnull (result, row, col_num)) + { + return GNUNET_NO; + } + data_sz = PQgetlength (result, row, col_num); FAIL_IF (0 > data_sz); FAIL_IF (sizeof(header) > (size_t) data_sz); @@ -1318,6 +1323,30 @@ extract_array_generic ( } return GNUNET_OK; + case TALER_PQ_array_of_cs_r_pub: + if (NULL != dst_size) + *dst_size = sizeof(struct GNUNET_CRYPTO_CSPublicRPairP) * (header.dim); + out = GNUNET_new_array (header.dim, + struct GNUNET_CRYPTO_CSPublicRPairP); + *((void **) dst) = out; + for (uint32_t i = 0; i < header.dim; i++) + { + uint32_t val; + size_t sz; + + GNUNET_memcpy (&val, + in, + sizeof(val)); + sz = ntohl (val); + FAIL_IF (sz != sizeof(struct GNUNET_CRYPTO_CSPublicRPairP)); + in += sizeof(uint32_t); + *(struct GNUNET_CRYPTO_CSPublicRPairP *) out = + *(struct GNUNET_CRYPTO_CSPublicRPairP *) in; + in += sz; + out += sz; + } + return GNUNET_OK; + case TALER_PQ_array_of_blinded_denom_sig: { struct TALER_BlindedDenominationSignature *denom_sigs; @@ -1585,4 +1614,33 @@ TALER_PQ_result_spec_array_hash_code ( } +struct GNUNET_PQ_ResultSpec +TALER_PQ_result_spec_array_cs_r_pub ( + struct GNUNET_PQ_Context *db, + const char *name, + size_t *num, + struct GNUNET_CRYPTO_CSPublicRPairP **cs_r_pubs) +{ + struct ArrayResultCls *info = GNUNET_new (struct ArrayResultCls); + + info->num = num; + info->typ = TALER_PQ_array_of_cs_r_pub; + GNUNET_assert (GNUNET_OK == + GNUNET_PQ_get_oid_by_name (db, + "bytea", + &info->oid)); + { + struct GNUNET_PQ_ResultSpec res = { + .conv = extract_array_generic, + .cleaner = array_cleanup, + .dst = (void *) cs_r_pubs, + .fname = name, + .cls = info + }; + + return res; + } +} + + /* end of pq_result_helper.c */ diff --git a/src/testing/testing_api_cmd_age_withdraw.c b/src/testing/testing_api_cmd_age_withdraw.c @@ -118,18 +118,21 @@ struct AgeWithdrawState */ struct TALER_EXCHANGE_DenomPublicKey *denoms_pub; + /** - * The seeds for the batch of @e num_coins coin candidates. - * It contains kappa secrets, from which we will have - * to disclose kappa-1 in a subsequent reveal-withdraw operation. + * The master seed from which all the other seeds are derived from */ - struct TALER_KappaWithdrawMasterSeedP seeds; + struct TALER_WithdrawMasterSeedP seed; /** - * The TALER_CNC_KAPPA tuple of @e num_coins secrets, - * derived from the @e seeds; + * The #TALER_CNC_KAPPA seeds derived from @e seed */ - struct TALER_PlanchetMasterSecretP *secrets[TALER_CNC_KAPPA]; + struct TALER_KappaWithdrawMasterSeedP kappa_seed; + + /** + * The master seed from which all the other seeds are derived from + */ + struct TALER_BlindingMasterSeedP blinding_seed; /** * The output state of @e num_coins coins, calculated during the @@ -199,6 +202,7 @@ age_withdraw_cb ( aws->h_planchets = response->details.created.h_planchets; aws->reserve_history.details.withdraw.h_planchets = aws->h_planchets; aws->reserve_history.details.withdraw.noreveal_index = aws->noreveal_index; + aws->kappa_seed = response->details.created.kappa_seed; GNUNET_assert (aws->num_coins == response->details.created.num_coins); for (size_t n = 0; n < aws->num_coins; n++) @@ -287,32 +291,9 @@ age_withdraw_run ( aws->denoms_pub = GNUNET_new_array (aws->num_coins, struct TALER_EXCHANGE_DenomPublicKey); - { - struct TALER_WithdrawMasterSeedP seed; - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, - &seed, - sizeof(seed)); - TALER_expand_seed_to_kappa_seeds (&seed, - &aws->seeds); - - /** - * Although the secrets are calculated within the call to - * TALER_EXCHANGE_withdraw_with_age_proof below, - * we do the same here in order to be able to store - * the individual coin's planchet secrets. - */ - for (size_t k = 0; k < TALER_CNC_KAPPA; k++) - { - aws->secrets[k] = GNUNET_new_array ( - aws->num_coins, - struct TALER_PlanchetMasterSecretP); - - TALER_expand_withdraw_secrets ( - aws->num_coins, - &aws->seeds.tuple[k], - aws->secrets[k]); - } - } + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &aws->seed, + sizeof(aws->seed)); for (unsigned int i = 0; i<aws->num_coins; i++) { @@ -369,10 +350,10 @@ age_withdraw_run ( keys, TALER_TESTING_get_exchange_url (is), rp, - aws->max_age, aws->num_coins, aws->denoms_pub, - &aws->seeds, + &aws->seed, + aws->max_age, &age_withdraw_cb, aws); @@ -407,13 +388,6 @@ age_withdraw_cleanup ( aws->handle = NULL; } - for (size_t k = 0; k < TALER_CNC_KAPPA; k++) - { - if (NULL != aws->secrets[k]) - GNUNET_free (aws->secrets[k]); - aws->secrets[k] = NULL; - } - if (NULL != aws->denoms_pub) { for (size_t n = 0; n < aws->num_coins; n++) @@ -460,7 +434,6 @@ age_withdraw_traits ( unsigned int idx) { struct AgeWithdrawState *aws = cls; - uint8_t k = aws->noreveal_index; struct CoinOutputState *out = &aws->coin_outputs[idx]; struct TALER_EXCHANGE_WithdrawCoinPrivateDetails *details = &aws->coin_outputs[idx].details; @@ -483,9 +456,11 @@ age_withdraw_traits ( TALER_TESTING_make_trait_exchange_url (aws->exchange_url), TALER_TESTING_make_trait_coin_priv (idx, &details->coin_priv), - TALER_TESTING_make_trait_kappa_seed (&aws->seeds), + TALER_TESTING_make_trait_withdraw_seed (&aws->seed), + /* FIXME[oec]: needed!? TALER_TESTING_make_trait_planchet_secrets (idx, &aws->secrets[k][idx]), + */ TALER_TESTING_make_trait_blinding_key (idx, &details->blinding_key), TALER_TESTING_make_trait_exchange_wd_value (idx, @@ -728,7 +703,7 @@ age_reveal_withdraw_run ( if (aws->noreveal_index == k) continue; - revealed_seeds.tuple[j] = aws->seeds.tuple[k]; + revealed_seeds.tuple[j] = aws->kappa_seed.tuple[k]; j++; } @@ -742,7 +717,6 @@ age_reveal_withdraw_run ( age_reveal_withdraw_cb, awrs); } - } diff --git a/src/testing/testing_api_cmd_batch_withdraw.c b/src/testing/testing_api_cmd_batch_withdraw.c @@ -367,9 +367,9 @@ batch_withdraw_run (void *cls, TALER_TESTING_get_exchange_url (is), rp, ws->num_coins, - 0, denoms_pub, &ws->seed, + 0, &batch_withdraw_cb, ws); if (NULL == ws->wsh) diff --git a/src/testing/testing_api_cmd_withdraw.c b/src/testing/testing_api_cmd_withdraw.c @@ -138,6 +138,11 @@ struct WithdrawState struct TALER_WithdrawMasterSeedP seed; /** + * Blinding seed for the blinding preparation for CS. + */ + struct TALER_BlindingMasterSeedP blinding_seed; + + /** * An age > 0 signifies age restriction is required */ uint8_t age; @@ -398,11 +403,12 @@ withdraw_run (void *cls, = TALER_reserve_make_payto (ws->exchange_url, &ws->reserve_pub); - if (NULL == ws->reuse_coin_key_ref) - { - TALER_withdraw_master_seed_setup_random (&ws->seed); - } - else + + TALER_withdraw_master_seed_setup_random (&ws->seed); + TALER_cs_withdraw_seed_to_blinding_seed (&ws->seed, + &ws->blinding_seed); + + if (NULL != ws->reuse_coin_key_ref) { const struct TALER_WithdrawMasterSeedP *seed; const struct TALER_TESTING_Command *cref; @@ -455,15 +461,16 @@ withdraw_run (void *cls, ws->reserve_history.details.withdraw.fee = ws->pk->fees.withdraw; - ws->wsh = TALER_EXCHANGE_withdraw ( + ws->wsh = TALER_EXCHANGE_withdraw_extra_blinding_seed ( TALER_TESTING_interpreter_get_context (is), TALER_TESTING_get_keys (is), TALER_TESTING_get_exchange_url (is), rp, 1, - ws->age, ws->pk, &ws->seed, + &ws->blinding_seed, + ws->age, &withdraw_cb, ws); @@ -540,6 +547,7 @@ withdraw_traits (void *cls, TALER_TESTING_make_trait_coin_priv (0 /* only one coin */, &ws->coin_priv), TALER_TESTING_make_trait_withdraw_seed (&ws->seed), + TALER_TESTING_make_trait_blinding_seed (&ws->blinding_seed), TALER_TESTING_make_trait_withdraw_commitment (&ws->h_planchets), TALER_TESTING_make_trait_blinding_key (0 /* only one coin */, &ws->bks), diff --git a/src/util/crypto.c b/src/util/crypto.c @@ -180,7 +180,7 @@ TALER_expand_withdraw_secrets ( void -TALER_expand_seed_to_kappa_seeds ( +TALER_expand_seed_to_kappa_seed ( const struct TALER_WithdrawMasterSeedP *seed, struct TALER_KappaWithdrawMasterSeedP *seeds) { @@ -292,6 +292,85 @@ TALER_cs_withdraw_nonce_derive ( void +TALER_cs_withdraw_seed_to_blinding_seed ( + const struct TALER_WithdrawMasterSeedP *seed, + struct TALER_BlindingMasterSeedP *blinding_seed) +{ + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (blinding_seed, + sizeof (*blinding_seed), + "blinding", + strlen ("blinding"), + seed, + sizeof(*seed), + NULL, + 0)); +} + + +void +TALER_cs_nonce_derive_indexed ( + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, + uint32_t index, + struct GNUNET_CRYPTO_CsSessionNonce *nonce) +{ + uint32_t be_salt = htonl (index); + const char *operation = for_melt ? "refresh-n" : "withdraw-n"; + + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (nonce, + sizeof (*nonce), + &be_salt, + sizeof (be_salt), + operation, + strlen (operation), + seed, + sizeof(*seed), + NULL, + 0)); +} + + +void +TALER_cs_derive_nonces_from_seed ( + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, + size_t num, + const uint32_t indices[static num], + struct GNUNET_CRYPTO_CsSessionNonce nonces[static num]) +{ + GNUNET_assert (TALER_MAX_FRESH_COINS > num); + + for (size_t i = 0; i < num; i++) + TALER_cs_nonce_derive_indexed ( + seed, + for_melt, + indices[i], + &nonces[i]); +} + + +void +TALER_cs_derive_blind_nonces_from_seed ( + const struct TALER_BlindingMasterSeedP *seed, + bool for_melt, + size_t num, + const uint32_t indices[static num], + union GNUNET_CRYPTO_BlindSessionNonce nonces[static num]) +{ + GNUNET_assert (TALER_MAX_FRESH_COINS > num); + + for (size_t i = 0; i < num; i++) + TALER_cs_nonce_derive_indexed ( + seed, + for_melt, + indices[i], + &nonces[i].cs_nonce); +} + + +void TALER_cs_refresh_nonce_derive ( const struct TALER_RefreshMasterSecretP *rms, uint32_t coin_num_salt, diff --git a/src/util/test_crypto.c b/src/util/test_crypto.c @@ -256,6 +256,7 @@ test_planchets_cs (uint8_t age) TALER_denom_priv_create (&dk_priv, &dk_pub, GNUNET_CRYPTO_BSA_CS)); +#pragma message "phase out TALER_cs_withdraw_nonce_derive" TALER_cs_withdraw_nonce_derive ( &ps, &nonce.cs_nonce); diff --git a/src/util/test_helper_cs.c b/src/util/test_helper_cs.c @@ -287,6 +287,7 @@ test_r_derive (struct TALER_CRYPTO_CsDenominationHelper *dh) continue; GNUNET_assert (GNUNET_CRYPTO_BSA_CS == keys[i].denom_pub.bsign_pub_key->cipher); +#pragma message "phase out TALER_cs_withdraw_nonce_derive" TALER_cs_withdraw_nonce_derive ( &ps, &nonce.cs_nonce); diff --git a/src/util/wallet_signatures.c b/src/util/wallet_signatures.c @@ -656,6 +656,15 @@ struct TALER_WithdrawRequestPS struct TALER_HashBlindedPlanchetsP h_planchets GNUNET_PACKED; /** + * If any of the denominations is of cipher type Clause-Schnorr, + * the client had to call /blinding-prepare prior to the withdraw + * to retrieve public R-values for the CS signature scheme. + * The input seed for that request must be provided here. + * Otherwise, if no CS denomination is used, the struct must be all zeros. + */ + struct TALER_BlindingMasterSeedP blinding_seed; + + /** * Maximum age group that the coins are going to be restricted to. * MUST be 0 if no age restriction applies. */ @@ -725,6 +734,7 @@ TALER_wallet_withdraw_sign ( const struct TALER_Amount *amount, const struct TALER_Amount *fee, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePrivateKeyP *reserve_priv, @@ -748,6 +758,8 @@ TALER_wallet_withdraw_sign ( amount); TALER_amount_hton (&req.fee, fee); + if (NULL != blinding_seed) + req.blinding_seed = *blinding_seed; GNUNET_CRYPTO_eddsa_sign ( &reserve_priv->eddsa_priv, @@ -762,6 +774,7 @@ TALER_wallet_withdraw_verify ( const struct TALER_Amount *amount, const struct TALER_Amount *fee, const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_BlindingMasterSeedP *blinding_seed, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePublicKeyP *reserve_pub, @@ -785,6 +798,8 @@ TALER_wallet_withdraw_verify ( amount); TALER_amount_hton (&req.fee, fee); + if (NULL != blinding_seed) + req.blinding_seed = *blinding_seed; return GNUNET_CRYPTO_eddsa_verify ( TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW,