Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2014-2022 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify it under the
6 : terms of the GNU Affero General Public License as published by the Free Software
7 : Foundation; either version 3, or (at your option) any later version.
8 :
9 : TALER is distributed in the hope that it will be useful, but WITHOUT ANY
10 : WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 : A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
12 :
13 : You should have received a copy of the GNU Affero General Public License along with
14 : TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
15 : */
16 : /**
17 : * @file taler-exchange-httpd_melt.c
18 : * @brief Handle melt requests
19 : * @author Florian Dold
20 : * @author Benedikt Mueller
21 : * @author Christian Grothoff
22 : */
23 : #include "platform.h"
24 : #include <gnunet/gnunet_util_lib.h>
25 : #include <jansson.h>
26 : #include <microhttpd.h>
27 : #include "taler_json_lib.h"
28 : #include "taler_mhd_lib.h"
29 : #include "taler-exchange-httpd_mhd.h"
30 : #include "taler-exchange-httpd_melt.h"
31 : #include "taler-exchange-httpd_responses.h"
32 : #include "taler-exchange-httpd_keys.h"
33 : #include "taler_exchangedb_lib.h"
34 :
35 :
36 : /**
37 : * Send a response to a "melt" request.
38 : *
39 : * @param connection the connection to send the response to
40 : * @param rc value the client committed to
41 : * @param noreveal_index which index will the client not have to reveal
42 : * @return a MHD status code
43 : */
44 : static MHD_RESULT
45 0 : reply_melt_success (struct MHD_Connection *connection,
46 : const struct TALER_RefreshCommitmentP *rc,
47 : uint32_t noreveal_index)
48 : {
49 : struct TALER_ExchangePublicKeyP pub;
50 : struct TALER_ExchangeSignatureP sig;
51 : enum TALER_ErrorCode ec;
52 :
53 0 : if (TALER_EC_NONE !=
54 0 : (ec = TALER_exchange_online_melt_confirmation_sign (
55 : &TEH_keys_exchange_sign_,
56 : rc,
57 : noreveal_index,
58 : &pub,
59 : &sig)))
60 : {
61 0 : return TALER_MHD_reply_with_ec (connection,
62 : ec,
63 : NULL);
64 : }
65 0 : return TALER_MHD_REPLY_JSON_PACK (
66 : connection,
67 : MHD_HTTP_OK,
68 : GNUNET_JSON_pack_uint64 ("noreveal_index",
69 : noreveal_index),
70 : GNUNET_JSON_pack_data_auto ("exchange_sig",
71 : &sig),
72 : GNUNET_JSON_pack_data_auto ("exchange_pub",
73 : &pub));
74 : }
75 :
76 :
77 : /**
78 : * Context for the melt operation.
79 : */
80 : struct MeltContext
81 : {
82 :
83 : /**
84 : * noreveal_index is only initialized during
85 : * #melt_transaction().
86 : */
87 : struct TALER_EXCHANGEDB_Refresh refresh_session;
88 :
89 : /**
90 : * UUID of the coin in the known_coins table.
91 : */
92 : uint64_t known_coin_id;
93 :
94 : /**
95 : * Information about the @e coin's value.
96 : */
97 : struct TALER_Amount coin_value;
98 :
99 : /**
100 : * Information about the @e coin's refresh fee.
101 : */
102 : struct TALER_Amount coin_refresh_fee;
103 :
104 : /**
105 : * Refresh master secret, if any of the fresh denominations use CS.
106 : */
107 : struct TALER_RefreshMasterSecretP rms;
108 :
109 : /**
110 : * Set to true if this coin's denomination was revoked and the operation
111 : * is thus only allowed for zombie coins where the transaction
112 : * history includes a #TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP.
113 : */
114 : bool zombie_required;
115 :
116 : /**
117 : * We already checked and noticed that the coin is known. Hence we
118 : * can skip the "ensure_coin_known" step of the transaction.
119 : */
120 : bool coin_is_dirty;
121 :
122 : /**
123 : * True if @e rms is missing.
124 : */
125 : bool no_rms;
126 : };
127 :
128 :
129 : /**
130 : * Execute a "melt". We have been given a list of valid
131 : * coins and a request to melt them into the given @a
132 : * refresh_session_pub. Check that the coins all have the required
133 : * value left and if so, store that they have been melted and confirm
134 : * the melting operation to the client.
135 : *
136 : * If it returns a non-error code, the transaction logic MUST NOT
137 : * queue a MHD response. IF it returns an hard error, the transaction
138 : * logic MUST queue a MHD response and set @a mhd_ret. If it returns
139 : * the soft error code, the function MAY be called again to retry and
140 : * MUST not queue a MHD response.
141 : *
142 : * @param cls our `struct MeltContext`
143 : * @param connection MHD request which triggered the transaction
144 : * @param[out] mhd_ret set to MHD response status for @a connection,
145 : * if transaction failed (!)
146 : * @return transaction status
147 : */
148 : static enum GNUNET_DB_QueryStatus
149 0 : melt_transaction (void *cls,
150 : struct MHD_Connection *connection,
151 : MHD_RESULT *mhd_ret)
152 : {
153 0 : struct MeltContext *rmc = cls;
154 : enum GNUNET_DB_QueryStatus qs;
155 : bool balance_ok;
156 :
157 : /* pick challenge and persist it */
158 : rmc->refresh_session.noreveal_index
159 0 : = GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG,
160 : TALER_CNC_KAPPA);
161 :
162 0 : if (0 >
163 0 : (qs = TEH_plugin->do_melt (TEH_plugin->cls,
164 0 : rmc->no_rms
165 : ? NULL
166 : : &rmc->rms,
167 : &rmc->refresh_session,
168 : rmc->known_coin_id,
169 : &rmc->zombie_required,
170 : &balance_ok)))
171 : {
172 0 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
173 : {
174 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
175 : MHD_HTTP_INTERNAL_SERVER_ERROR,
176 : TALER_EC_GENERIC_DB_STORE_FAILED,
177 : "melt");
178 0 : return GNUNET_DB_STATUS_HARD_ERROR;
179 : }
180 0 : return qs;
181 : }
182 0 : GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
183 0 : if (rmc->zombie_required)
184 : {
185 0 : GNUNET_break_op (0);
186 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
187 : MHD_HTTP_BAD_REQUEST,
188 : TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE,
189 : NULL);
190 0 : return GNUNET_DB_STATUS_HARD_ERROR;
191 : }
192 0 : if (! balance_ok)
193 : {
194 0 : GNUNET_break_op (0);
195 : *mhd_ret
196 0 : = TEH_RESPONSE_reply_coin_insufficient_funds (
197 : connection,
198 : TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS,
199 0 : &rmc->refresh_session.coin.denom_pub_hash,
200 0 : &rmc->refresh_session.coin.coin_pub);
201 0 : return GNUNET_DB_STATUS_HARD_ERROR;
202 : }
203 : /* All good, commit, final response will be generated by caller */
204 0 : TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT]++;
205 0 : return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
206 : }
207 :
208 :
209 : /**
210 : * Handle a "melt" request after the first parsing has
211 : * happened. Performs the database transactions.
212 : *
213 : * @param connection the MHD connection to handle
214 : * @param[in,out] rmc details about the melt request
215 : * @return MHD result code
216 : */
217 : static MHD_RESULT
218 0 : database_melt (struct MHD_Connection *connection,
219 : struct MeltContext *rmc)
220 : {
221 0 : if (GNUNET_SYSERR ==
222 0 : TEH_plugin->preflight (TEH_plugin->cls))
223 : {
224 0 : GNUNET_break (0);
225 0 : return TALER_MHD_reply_with_error (connection,
226 : MHD_HTTP_INTERNAL_SERVER_ERROR,
227 : TALER_EC_GENERIC_DB_START_FAILED,
228 : "preflight failure");
229 : }
230 :
231 : /* first, make sure coin is known */
232 0 : if (! rmc->coin_is_dirty)
233 : {
234 0 : MHD_RESULT mhd_ret = MHD_NO;
235 : enum GNUNET_DB_QueryStatus qs;
236 :
237 0 : for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++)
238 : {
239 0 : qs = TEH_make_coin_known (&rmc->refresh_session.coin,
240 : connection,
241 : &rmc->known_coin_id,
242 : &mhd_ret);
243 0 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
244 0 : break;
245 : }
246 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
247 : {
248 0 : GNUNET_break (0);
249 0 : return TALER_MHD_reply_with_error (connection,
250 : MHD_HTTP_INTERNAL_SERVER_ERROR,
251 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
252 : "make_coin_known");
253 : }
254 0 : if (qs < 0)
255 0 : return mhd_ret;
256 : }
257 :
258 : /* run main database transaction */
259 : {
260 : MHD_RESULT mhd_ret;
261 :
262 0 : if (GNUNET_OK !=
263 0 : TEH_DB_run_transaction (connection,
264 : "run melt",
265 : TEH_MT_REQUEST_MELT,
266 : &mhd_ret,
267 : &melt_transaction,
268 : rmc))
269 0 : return mhd_ret;
270 : }
271 :
272 : /* Success. Generate ordinary response. */
273 0 : return reply_melt_success (connection,
274 0 : &rmc->refresh_session.rc,
275 : rmc->refresh_session.noreveal_index);
276 : }
277 :
278 :
279 : /**
280 : * Check for information about the melted coin's denomination,
281 : * extracting its validity status and fee structure.
282 : *
283 : * @param connection HTTP connection we are handling
284 : * @param rmc parsed request information
285 : * @return MHD status code
286 : */
287 : static MHD_RESULT
288 0 : check_melt_valid (struct MHD_Connection *connection,
289 : struct MeltContext *rmc)
290 : {
291 : /* Baseline: check if deposits/refreshs are generally
292 : simply still allowed for this denomination */
293 : struct TEH_DenominationKey *dk;
294 : MHD_RESULT mret;
295 :
296 0 : dk = TEH_keys_denomination_by_hash (
297 0 : &rmc->refresh_session.coin.denom_pub_hash,
298 : connection,
299 : &mret);
300 0 : if (NULL == dk)
301 0 : return mret;
302 :
303 0 : if (GNUNET_TIME_absolute_is_past (dk->meta.expire_legal.abs_time))
304 : {
305 : /* Way too late now, even zombies have expired */
306 0 : return TEH_RESPONSE_reply_expired_denom_pub_hash (
307 : connection,
308 0 : &rmc->refresh_session.coin.denom_pub_hash,
309 : TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
310 : "MELT");
311 : }
312 :
313 0 : if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time))
314 : {
315 : /* This denomination is not yet valid */
316 0 : return TEH_RESPONSE_reply_expired_denom_pub_hash (
317 : connection,
318 0 : &rmc->refresh_session.coin.denom_pub_hash,
319 : TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
320 : "MELT");
321 : }
322 :
323 0 : rmc->coin_refresh_fee = dk->meta.fees.refresh;
324 0 : rmc->coin_value = dk->meta.value;
325 :
326 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
327 : "Melted coin's denomination is worth %s\n",
328 : TALER_amount2s (&dk->meta.value));
329 :
330 : /* sanity-check that "total melt amount > melt fee" */
331 0 : if (0 <
332 0 : TALER_amount_cmp (&rmc->coin_refresh_fee,
333 0 : &rmc->refresh_session.amount_with_fee))
334 : {
335 0 : GNUNET_break_op (0);
336 0 : return TALER_MHD_reply_with_error (connection,
337 : MHD_HTTP_BAD_REQUEST,
338 : TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION,
339 : NULL);
340 : }
341 0 : switch (dk->denom_pub.cipher)
342 : {
343 0 : case TALER_DENOMINATION_RSA:
344 0 : TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++;
345 0 : break;
346 0 : case TALER_DENOMINATION_CS:
347 0 : TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++;
348 0 : break;
349 0 : default:
350 0 : break;
351 : }
352 0 : if (GNUNET_OK !=
353 0 : TALER_test_coin_valid (&rmc->refresh_session.coin,
354 0 : &dk->denom_pub))
355 : {
356 0 : GNUNET_break_op (0);
357 0 : return TALER_MHD_reply_with_error (connection,
358 : MHD_HTTP_FORBIDDEN,
359 : TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID,
360 : NULL);
361 : }
362 :
363 : /* verify signature of coin for melt operation */
364 0 : TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
365 0 : if (GNUNET_OK !=
366 0 : TALER_wallet_melt_verify (&rmc->refresh_session.amount_with_fee,
367 0 : &rmc->coin_refresh_fee,
368 0 : &rmc->refresh_session.rc,
369 0 : &rmc->refresh_session.coin.denom_pub_hash,
370 0 : &rmc->refresh_session.coin.h_age_commitment,
371 0 : &rmc->refresh_session.coin.coin_pub,
372 0 : &rmc->refresh_session.coin_sig))
373 : {
374 0 : GNUNET_break_op (0);
375 0 : return TALER_MHD_reply_with_error (connection,
376 : MHD_HTTP_FORBIDDEN,
377 : TALER_EC_EXCHANGE_MELT_COIN_SIGNATURE_INVALID,
378 : NULL);
379 : }
380 :
381 0 : if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time))
382 : {
383 : /* We are past deposit expiration time, but maybe this is a zombie? */
384 : struct TALER_DenominationHashP denom_hash;
385 : enum GNUNET_DB_QueryStatus qs;
386 :
387 : /* Check that the coin is dirty (we have seen it before), as we will
388 : not just allow melting of a *fresh* coin where the denomination was
389 : revoked (those must be recouped) */
390 0 : qs = TEH_plugin->get_coin_denomination (
391 0 : TEH_plugin->cls,
392 0 : &rmc->refresh_session.coin.coin_pub,
393 : &rmc->known_coin_id,
394 : &denom_hash);
395 0 : if (0 > qs)
396 : {
397 : /* There is no good reason for a serialization failure here: */
398 0 : GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
399 0 : return TALER_MHD_reply_with_error (connection,
400 : MHD_HTTP_INTERNAL_SERVER_ERROR,
401 : TALER_EC_GENERIC_DB_FETCH_FAILED,
402 : "coin denomination");
403 : }
404 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
405 : {
406 : /* We never saw this coin before, so _this_ justification is not OK */
407 0 : return TEH_RESPONSE_reply_expired_denom_pub_hash (
408 : connection,
409 0 : &rmc->refresh_session.coin.denom_pub_hash,
410 : TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
411 : "MELT");
412 : }
413 : /* Minor optimization: no need to run the
414 : "ensure_coin_known" part of the transaction */
415 0 : rmc->coin_is_dirty = true;
416 : /* sanity check */
417 0 : if (0 !=
418 0 : GNUNET_memcmp (&denom_hash,
419 : &rmc->refresh_session.coin.denom_pub_hash))
420 : {
421 0 : GNUNET_break_op (0);
422 0 : return TALER_MHD_reply_with_ec (
423 : connection,
424 : TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY,
425 : TALER_B2S (&denom_hash));
426 : }
427 0 : rmc->zombie_required = true; /* check later that zombie is satisfied */
428 : }
429 :
430 0 : return database_melt (connection,
431 : rmc);
432 : }
433 :
434 :
435 : MHD_RESULT
436 0 : TEH_handler_melt (struct MHD_Connection *connection,
437 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
438 : const json_t *root)
439 : {
440 : struct MeltContext rmc;
441 : struct GNUNET_JSON_Specification spec[] = {
442 0 : TALER_JSON_spec_denom_sig ("denom_sig",
443 : &rmc.refresh_session.coin.denom_sig),
444 0 : GNUNET_JSON_spec_fixed_auto ("denom_pub_hash",
445 : &rmc.refresh_session.coin.denom_pub_hash),
446 0 : GNUNET_JSON_spec_mark_optional (
447 : GNUNET_JSON_spec_fixed_auto ("age_commitment_hash",
448 : &rmc.refresh_session.coin.h_age_commitment),
449 : &rmc.refresh_session.coin.no_age_commitment),
450 0 : GNUNET_JSON_spec_fixed_auto ("confirm_sig",
451 : &rmc.refresh_session.coin_sig),
452 0 : TALER_JSON_spec_amount ("value_with_fee",
453 : TEH_currency,
454 : &rmc.refresh_session.amount_with_fee),
455 0 : GNUNET_JSON_spec_fixed_auto ("rc",
456 : &rmc.refresh_session.rc),
457 0 : GNUNET_JSON_spec_mark_optional (
458 : GNUNET_JSON_spec_fixed_auto ("rms",
459 : &rmc.rms),
460 : &rmc.no_rms),
461 0 : GNUNET_JSON_spec_end ()
462 : };
463 :
464 0 : memset (&rmc, 0, sizeof (rmc));
465 0 : rmc.refresh_session.coin.coin_pub = *coin_pub;
466 :
467 : {
468 : enum GNUNET_GenericReturnValue ret;
469 0 : ret = TALER_MHD_parse_json_data (connection,
470 : root,
471 : spec);
472 0 : if (GNUNET_OK != ret)
473 0 : return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES;
474 : }
475 :
476 : {
477 : MHD_RESULT res;
478 :
479 0 : res = check_melt_valid (connection,
480 : &rmc);
481 0 : GNUNET_JSON_parse_free (spec);
482 0 : return res;
483 : }
484 : }
485 :
486 :
487 : /* end of taler-exchange-httpd_melt.c */
|