/* This file is part of TALER Copyright (C) 2014-2017 Inria & GNUnet e.V. TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-exchange-httpd_refresh_reveal.c * @brief Handle /refresh/reveal requests * @author Florian Dold * @author Benedikt Mueller * @author Christian Grothoff */ #include "platform.h" #include #include #include #include "taler-exchange-httpd_parsing.h" #include "taler-exchange-httpd_mhd.h" #include "taler-exchange-httpd_refresh_reveal.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_keystate.h" /** * Send a response for "/refresh/reveal". * * @param connection the connection to send the response to * @param num_newcoins number of new coins for which we reveal data * @param sigs array of @a num_newcoins signatures revealed * @return a MHD result code */ static int reply_refresh_reveal_success (struct MHD_Connection *connection, unsigned int num_newcoins, const struct TALER_DenominationSignature *sigs) { json_t *root; json_t *obj; json_t *list; int ret; list = json_array (); for (unsigned int newcoin_index = 0; newcoin_index < num_newcoins; newcoin_index++) { obj = json_object (); json_object_set_new (obj, "ev_sig", GNUNET_JSON_from_rsa_signature (sigs[newcoin_index].rsa_signature)); GNUNET_assert (0 == json_array_append_new (list, obj)); } root = json_object (); json_object_set_new (root, "ev_sigs", list); ret = TEH_RESPONSE_reply_json (connection, root, MHD_HTTP_OK); json_decref (root); return ret; } /** * Send a response for a failed "/refresh/reveal", where the * revealed value(s) do not match the original commitment. * * @param connection the connection to send the response to * @param session info about session * @param commit_coins array of @a num_newcoins committed envelopes at offset @a gamma * @param denom_pubs array of @a num_newcoins denomination keys for the new coins * @param gamma_tp transfer public key at offset @a gamma * @return a MHD result code */ static int reply_refresh_reveal_missmatch (struct MHD_Connection *connection, const struct TALER_EXCHANGEDB_RefreshSession *session, const struct TALER_EXCHANGEDB_RefreshCommitCoin *commit_coins, const struct TALER_DenominationPublicKey *denom_pubs, const struct TALER_TransferPublicKeyP *gamma_tp) { json_t *info_new; json_t *info_commit_k; info_new = json_array (); info_commit_k = json_array (); for (unsigned int i=0;inum_newcoins;i++) { const struct TALER_EXCHANGEDB_RefreshCommitCoin *cc; json_t *cc_json; GNUNET_assert (0 == json_array_append_new (info_new, GNUNET_JSON_from_rsa_public_key (denom_pubs[i].rsa_public_key))); cc = &commit_coins[i]; cc_json = json_pack ("{s:o}", "coin_ev", GNUNET_JSON_from_data (cc->coin_ev, cc->coin_ev_size)); GNUNET_assert (0 == json_array_append_new (info_commit_k, cc_json)); } return TEH_RESPONSE_reply_json_pack (connection, MHD_HTTP_CONFLICT, "{s:s, s:I, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:i}", "error", "commitment violation", "code", (json_int_t) TALER_EC_REFRESH_REVEAL_COMMITMENT_VIOLATION, "coin_sig", GNUNET_JSON_from_data_auto (&session->melt.coin_sig), "coin_pub", GNUNET_JSON_from_data_auto (&session->melt.coin.coin_pub), "melt_amount_with_fee", TALER_JSON_from_amount (&session->melt.amount_with_fee), "melt_fee", TALER_JSON_from_amount (&session->melt.melt_fee), "newcoin_infos", info_new, "commit_infos", info_commit_k, "gamma_tp", GNUNET_JSON_from_data_auto (gamma_tp), "gamma", (int) session->noreveal_index); } /** * Check if the given @a transfer_privs correspond to an honest * commitment for the given session. * Checks that the transfer private keys match their commitments. * Then derives the shared secret for each #TALER_CNC_KAPPA, and check that they match. * * @param connection the MHD connection to handle * @param session database connection to use * @param session_hash hash of session to query * @param off commitment offset to check * @param transfer_priv private transfer key * @param melt information about the melted coin * @param num_newcoins number of newcoins being generated * @param denom_pubs array of @a num_newcoins keys for the new coins * @param hash_context hash context to update by hashing in the data * from this offset * @return #GNUNET_OK if the committment was honest, * #GNUNET_NO if there was a problem and we generated an error message * #GNUNET_SYSERR if we could not even generate an error message */ static int check_commitment (struct MHD_Connection *connection, struct TALER_EXCHANGEDB_Session *session, const struct GNUNET_HashCode *session_hash, unsigned int off, const struct TALER_TransferPrivateKeyP *transfer_priv, const struct TALER_EXCHANGEDB_RefreshMelt *melt, unsigned int num_newcoins, const struct TALER_DenominationPublicKey *denom_pubs, struct GNUNET_HashContext *hash_context) { struct TALER_TransferSecretP transfer_secret; TALER_link_reveal_transfer_secret (transfer_priv, &melt->coin.coin_pub, &transfer_secret); /* Check that the commitments for all new coins were correct */ for (unsigned int j = 0; j < num_newcoins; j++) { struct TALER_FreshCoinP fc; struct TALER_CoinSpendPublicKeyP coin_pub; struct GNUNET_HashCode h_msg; char *buf; size_t buf_len; TALER_setup_fresh_coin (&transfer_secret, j, &fc); GNUNET_CRYPTO_eddsa_key_get_public (&fc.coin_priv.eddsa_priv, &coin_pub.eddsa_pub); GNUNET_CRYPTO_hash (&coin_pub, sizeof (struct TALER_CoinSpendPublicKeyP), &h_msg); if (GNUNET_YES != GNUNET_CRYPTO_rsa_blind (&h_msg, &fc.blinding_key.bks, denom_pubs[j].rsa_public_key, &buf, &buf_len)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Blind failed (bad denomination key!?)\n"); return (MHD_YES == TEH_RESPONSE_reply_internal_error (connection, TALER_EC_REFRESH_REVEAL_BLINDING_ERROR, "Blinding error")) ? GNUNET_NO : GNUNET_SYSERR; } GNUNET_CRYPTO_hash_context_read (hash_context, buf, buf_len); GNUNET_free (buf); } return GNUNET_OK; } /** * State for a /refresh/reveal operation. */ struct RevealContext { /** * Hash of the refresh session. */ const struct GNUNET_HashCode *session_hash; /** * Database session used to execute the transaction. */ struct TALER_EXCHANGEDB_Session *session; /** * Session state from the database. */ struct TALER_EXCHANGEDB_RefreshSession refresh_session; /** * Array of denomination public keys used for the refresh. */ struct TALER_DenominationPublicKey *denom_pubs; /** * Envelopes with the signatures to be returned. */ struct TALER_DenominationSignature *ev_sigs; /** * Commitment data from the DB giving data about original * commitments, in particular the blinded envelopes (for * index gamma). */ struct TALER_EXCHANGEDB_RefreshCommitCoin *commit_coins; /** * Transfer public key associated with the gamma value * selected by the exchange. */ struct TALER_TransferPublicKeyP gamma_tp; /** * Transfer private keys revealed to us. */ struct TALER_TransferPrivateKeyP transfer_privs[TALER_CNC_KAPPA - 1]; }; /** * Exchange a coin as part of a refresh operation. Obtains the * envelope from the database and performs the signing operation. * * @param connection the MHD connection to handle * @param session database connection to use * @param session_hash hash of session to query * @param key_state key state to lookup denomination pubs * @param denom_pub denomination key for the coin to create * @param commit_coin the coin that was committed * @param coin_off number of the coin * @param[out] ev_sig set to signature over the coin upon success * @return database transaction status */ static enum GNUNET_DB_QueryStatus refresh_exchange_coin (struct MHD_Connection *connection, struct TALER_EXCHANGEDB_Session *session, const struct GNUNET_HashCode *session_hash, struct TEH_KS_StateHandle *key_state, const struct TALER_DenominationPublicKey *denom_pub, const struct TALER_EXCHANGEDB_RefreshCommitCoin *commit_coin, unsigned int coin_off, struct TALER_DenominationSignature *ev_sig) { struct TALER_EXCHANGEDB_DenominationKeyIssueInformation *dki; enum GNUNET_DB_QueryStatus qs; dki = TEH_KS_denomination_key_lookup (key_state, denom_pub, TEH_KS_DKU_WITHDRAW); if (NULL == dki) { GNUNET_break (0); ev_sig->rsa_signature = NULL; return GNUNET_DB_STATUS_HARD_ERROR; } qs = TEH_plugin->get_refresh_out (TEH_plugin->cls, session, session_hash, coin_off, ev_sig); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Returning cached reply for /refresh/reveal signature\n"); return qs; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) return qs; ev_sig->rsa_signature = GNUNET_CRYPTO_rsa_sign_blinded (dki->denom_priv.rsa_private_key, commit_coin->coin_ev, commit_coin->coin_ev_size); if (NULL == ev_sig->rsa_signature) { GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } qs = TEH_plugin->insert_refresh_out (TEH_plugin->cls, session, session_hash, coin_off, ev_sig); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); if (NULL != ev_sig->rsa_signature) { GNUNET_CRYPTO_rsa_signature_free (ev_sig->rsa_signature); ev_sig->rsa_signature = NULL; } } return qs; } /** * Cleanup state of the transaction stored in @a rc. * * @param rc context to clean up */ static void cleanup_rc (struct RevealContext *rc) { if (NULL != rc->denom_pubs) { for (unsigned int i=0;irefresh_session.num_newcoins;i++) if (NULL != rc->denom_pubs[i].rsa_public_key) GNUNET_CRYPTO_rsa_public_key_free (rc->denom_pubs[i].rsa_public_key); GNUNET_free (rc->denom_pubs); rc->denom_pubs = NULL; } if (NULL != rc->commit_coins) { for (unsigned int j=0;jrefresh_session.num_newcoins;j++) GNUNET_free_non_null (rc->commit_coins[j].coin_ev); GNUNET_free (rc->commit_coins); rc->commit_coins = NULL; } if (NULL != rc->ev_sigs) { for (unsigned int j=0;jrefresh_session.num_newcoins;j++) if (NULL != rc->ev_sigs[j].rsa_signature) GNUNET_CRYPTO_rsa_signature_free (rc->ev_sigs[j].rsa_signature); GNUNET_free (rc->ev_sigs); rc->ev_sigs = NULL; } if (NULL != rc->refresh_session.melt.coin.denom_sig.rsa_signature) { GNUNET_CRYPTO_rsa_signature_free (rc->refresh_session.melt.coin.denom_sig.rsa_signature); rc->refresh_session.melt.coin.denom_sig.rsa_signature = NULL; } if (NULL != rc->refresh_session.melt.coin.denom_pub.rsa_public_key) { GNUNET_CRYPTO_rsa_public_key_free (rc->refresh_session.melt.coin.denom_pub.rsa_public_key); rc->refresh_session.melt.coin.denom_pub.rsa_public_key = NULL; } } /** * Execute a "/refresh/reveal". The client is revealing to us the * transfer keys for @a #TALER_CNC_KAPPA-1 sets of coins. Verify that the * revealed transfer keys would allow linkage to the blinded coins, * and if so, return the signed coins for corresponding to the set of * coins that was not chosen. * * IF it returns a non-error code, the transaction logic MUST * NOT queue a MHD response. IF it returns an hard error, the * transaction logic MUST queue a MHD response and set @a mhd_ret. IF * it returns the soft error code, the function MAY be called again to * retry and MUST not queue a MHD response. * * @param cls closure of type `struct RevealContext` * @param connection MHD request which triggered the transaction * @param session database session to use * @param[out] mhd_ret set to MHD response status for @a connection, * if transaction failed (!) * @return transaction status */ static enum GNUNET_DB_QueryStatus refresh_reveal_transaction (void *cls, struct MHD_Connection *connection, struct TALER_EXCHANGEDB_Session *session, int *mhd_ret) { struct RevealContext *rc = cls; unsigned int off; struct GNUNET_HashContext *hash_context; struct GNUNET_HashCode sh_check; enum GNUNET_DB_QueryStatus qs; rc->session = session; qs = TEH_plugin->get_refresh_session (TEH_plugin->cls, session, rc->session_hash, &rc->refresh_session); if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { *mhd_ret = TEH_RESPONSE_reply_arg_invalid (connection, TALER_EC_REFRESH_REVEAL_SESSION_UNKNOWN, "session_hash"); return GNUNET_DB_STATUS_HARD_ERROR; } if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; if ( (GNUNET_DB_STATUS_HARD_ERROR == qs) || (rc->refresh_session.noreveal_index >= TALER_CNC_KAPPA) ) { GNUNET_break (0); cleanup_rc (rc); *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, TALER_EC_REFRESH_REVEAL_DB_FETCH_SESSION_ERROR); return GNUNET_DB_STATUS_HARD_ERROR; } rc->denom_pubs = GNUNET_new_array (rc->refresh_session.num_newcoins, struct TALER_DenominationPublicKey); qs = TEH_plugin->get_refresh_order (TEH_plugin->cls, session, rc->session_hash, rc->refresh_session.num_newcoins, rc->denom_pubs); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { cleanup_rc (rc); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; GNUNET_break (0); *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, TALER_EC_REFRESH_REVEAL_DB_FETCH_ORDER_ERROR); return GNUNET_DB_STATUS_HARD_ERROR; } hash_context = GNUNET_CRYPTO_hash_context_start (); /* first, iterate over transfer public keys for hash_context */ off = 0; for (unsigned int i=0;irefresh_session.noreveal_index) { off = 1; /* obtain gamma_tp from db */ qs = TEH_plugin->get_refresh_transfer_public_key (TEH_plugin->cls, session, rc->session_hash, &rc->gamma_tp); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { GNUNET_CRYPTO_hash_context_abort (hash_context); cleanup_rc (rc); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; GNUNET_break (0); *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, TALER_EC_REFRESH_REVEAL_DB_FETCH_TRANSFER_ERROR); return GNUNET_DB_STATUS_HARD_ERROR; } GNUNET_CRYPTO_hash_context_read (hash_context, &rc->gamma_tp, sizeof (struct TALER_TransferPublicKeyP)); } else { /* compute tp from private key */ struct TALER_TransferPublicKeyP tp; GNUNET_CRYPTO_ecdhe_key_get_public (&rc->transfer_privs[i - off].ecdhe_priv, &tp.ecdhe_pub); GNUNET_CRYPTO_hash_context_read (hash_context, &tp, sizeof (struct TALER_TransferPublicKeyP)); } } /* next, add all of the hashes from the denomination keys to the hash_context */ for (unsigned int i=0;irefresh_session.num_newcoins;i++) { char *buf; size_t buf_size; buf_size = GNUNET_CRYPTO_rsa_public_key_encode (rc->denom_pubs[i].rsa_public_key, &buf); GNUNET_CRYPTO_hash_context_read (hash_context, buf, buf_size); GNUNET_free (buf); } /* next, add public key of coin and amount being refreshed */ { struct TALER_AmountNBO melt_amountn; GNUNET_CRYPTO_hash_context_read (hash_context, &rc->refresh_session.melt.coin.coin_pub, sizeof (struct TALER_CoinSpendPublicKeyP)); TALER_amount_hton (&melt_amountn, &rc->refresh_session.melt.amount_with_fee); GNUNET_CRYPTO_hash_context_read (hash_context, &melt_amountn, sizeof (struct TALER_AmountNBO)); } rc->commit_coins = GNUNET_new_array (rc->refresh_session.num_newcoins, struct TALER_EXCHANGEDB_RefreshCommitCoin); off = 0; for (unsigned int i=0;irefresh_session.noreveal_index) { off = 1; /* obtain commit_coins for the selected gamma value from DB */ qs = TEH_plugin->get_refresh_commit_coins (TEH_plugin->cls, session, rc->session_hash, rc->refresh_session.num_newcoins, rc->commit_coins); if (0 >= qs) { cleanup_rc (rc); GNUNET_CRYPTO_hash_context_abort (hash_context); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; GNUNET_break (0); *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, TALER_EC_REFRESH_REVEAL_DB_FETCH_COMMIT_ERROR); return GNUNET_DB_STATUS_HARD_ERROR; } /* add envelopes to hash_context */ for (unsigned int j=0;jrefresh_session.num_newcoins;j++) { GNUNET_CRYPTO_hash_context_read (hash_context, rc->commit_coins[j].coin_ev, rc->commit_coins[j].coin_ev_size); } continue; } if (GNUNET_OK != (res = check_commitment (connection, session, rc->session_hash, i, &rc->transfer_privs[i - off], &rc->refresh_session.melt, rc->refresh_session.num_newcoins, rc->denom_pubs, hash_context))) { GNUNET_break_op (0); cleanup_rc (rc); GNUNET_CRYPTO_hash_context_abort (hash_context); *mhd_ret = (GNUNET_NO == res) ? MHD_YES : MHD_NO; return GNUNET_DB_STATUS_HARD_ERROR; } } /* Check session hash matches */ GNUNET_CRYPTO_hash_context_finish (hash_context, &sh_check); if (0 != memcmp (&sh_check, rc->session_hash, sizeof (struct GNUNET_HashCode))) { GNUNET_break_op (0); *mhd_ret = reply_refresh_reveal_missmatch (connection, &rc->refresh_session, rc->commit_coins, rc->denom_pubs, &rc->gamma_tp); cleanup_rc (rc); return GNUNET_DB_STATUS_HARD_ERROR; } /* Client request OK, sign coins */ rc->ev_sigs = GNUNET_new_array (rc->refresh_session.num_newcoins, struct TALER_DenominationSignature); { struct TEH_KS_StateHandle *key_state; key_state = TEH_KS_acquire (); for (unsigned int j=0;jrefresh_session.num_newcoins;j++) { qs = refresh_exchange_coin (connection, session, rc->session_hash, key_state, &rc->denom_pubs[j], &rc->commit_coins[j], j, &rc->ev_sigs[j]); if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) || (NULL == rc->ev_sigs[j].rsa_signature) ) { *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection, TALER_EC_REFRESH_REVEAL_SIGNING_ERROR); qs = GNUNET_DB_STATUS_HARD_ERROR; break; } } TEH_KS_release (key_state); } if (0 >= qs) { cleanup_rc (rc); return qs; } return qs; } /** * Handle a "/refresh/reveal" request. Parses the given JSON * transfer private keys and if successful, passes everything to * #TEH_DB_execute_refresh_reveal() which will verify that the * revealed information is valid then returns the signed refreshed * coins. * * @param connection the MHD connection to handle * @param session_hash hash identifying the melting session * @param tp_json private transfer keys in JSON format * @return MHD result code */ static int handle_refresh_reveal_json (struct MHD_Connection *connection, const struct GNUNET_HashCode *session_hash, const json_t *tp_json) { struct RevealContext rc; int mhd_ret; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "reveal request for session %s\n", GNUNET_h2s (session_hash)); memset (&rc, 0, sizeof (rc)); rc.session_hash = session_hash; for (unsigned int i = 0; i < TALER_CNC_KAPPA - 1; i++) { struct GNUNET_JSON_Specification tp_spec[] = { GNUNET_JSON_spec_fixed_auto (NULL, &rc.transfer_privs[i]), GNUNET_JSON_spec_end () }; int res; res = TEH_PARSE_json_array (connection, tp_json, tp_spec, i, -1); GNUNET_break_op (GNUNET_OK == res); if (GNUNET_OK != res) return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; } if (GNUNET_OK != TEH_DB_run_transaction (connection, &mhd_ret, &refresh_reveal_transaction, &rc)) { cleanup_rc (&rc); return mhd_ret; } mhd_ret = reply_refresh_reveal_success (connection, rc.refresh_session.num_newcoins, rc.ev_sigs); cleanup_rc (&rc); return mhd_ret; } /** * Handle a "/refresh/reveal" request. This time, the client reveals * the private transfer keys except for the cut-and-choose value * returned from "/refresh/melt". This function parses the revealed * keys and secrets and ultimately passes everything to * #TEH_DB_execute_refresh_reveal() which will verify that the * revealed information is valid then returns the signed refreshed * coins. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] connection_cls the connection's closure (can be updated) * @param upload_data upload data * @param[in,out] upload_data_size number of bytes (left) in @a upload_data * @return MHD result code */ int TEH_REFRESH_handler_refresh_reveal (struct TEH_RequestHandler *rh, struct MHD_Connection *connection, void **connection_cls, const char *upload_data, size_t *upload_data_size) { struct GNUNET_HashCode session_hash; int res; json_t *root; json_t *transfer_privs; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("session_hash", &session_hash), GNUNET_JSON_spec_json ("transfer_privs", &transfer_privs), GNUNET_JSON_spec_end () }; res = TEH_PARSE_post_json (connection, connection_cls, upload_data, upload_data_size, &root); if (GNUNET_SYSERR == res) return MHD_NO; if ( (GNUNET_NO == res) || (NULL == root) ) return MHD_YES; res = TEH_PARSE_json_data (connection, root, spec); json_decref (root); if (GNUNET_OK != res) { GNUNET_break_op (0); return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; } /* Determine dimensionality of the request (kappa and #old coins) */ /* Note we do +1 as 1 row (cut-and-choose!) is missing! */ if (TALER_CNC_KAPPA != json_array_size (transfer_privs) + 1) { GNUNET_JSON_parse_free (spec); GNUNET_break_op (0); return TEH_RESPONSE_reply_arg_invalid (connection, TALER_EC_REFRESH_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID, "transfer_privs"); } res = handle_refresh_reveal_json (connection, &session_hash, transfer_privs); GNUNET_JSON_parse_free (spec); return res; } /* end of taler-exchange-httpd_refresh_reveal.c */