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
6 : it under the terms of the GNU Affero General Public License as
7 : published by the Free Software Foundation; either version 3,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful,
11 : but WITHOUT ANY WARRANTY; without even the implied warranty
12 : of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 : See the GNU Affero General Public License for more details.
14 :
15 : You should have received a copy of the GNU Affero General
16 : Public License along with TALER; see the file COPYING. If not,
17 : see <http://www.gnu.org/licenses/>
18 : */
19 : /**
20 : * @file taler-exchange-httpd_withdraw.c
21 : * @brief Handle /reserves/$RESERVE_PUB/withdraw requests
22 : * @author Florian Dold
23 : * @author Benedikt Mueller
24 : * @author Christian Grothoff
25 : */
26 : #include "platform.h"
27 : #include <gnunet/gnunet_util_lib.h>
28 : #include <jansson.h>
29 : #include "taler_json_lib.h"
30 : #include "taler_kyclogic_lib.h"
31 : #include "taler_mhd_lib.h"
32 : #include "taler-exchange-httpd_withdraw.h"
33 : #include "taler-exchange-httpd_responses.h"
34 : #include "taler-exchange-httpd_keys.h"
35 :
36 :
37 : /**
38 : * Context for #withdraw_transaction.
39 : */
40 : struct WithdrawContext
41 : {
42 :
43 : /**
44 : * Hash of the (blinded) message to be signed by the Exchange.
45 : */
46 : struct TALER_BlindedCoinHashP h_coin_envelope;
47 :
48 : /**
49 : * Blinded planchet.
50 : */
51 : struct TALER_BlindedPlanchet blinded_planchet;
52 :
53 : /**
54 : * Set to the resulting signed coin data to be returned to the client.
55 : */
56 : struct TALER_EXCHANGEDB_CollectableBlindcoin collectable;
57 :
58 : /**
59 : * KYC status for the operation.
60 : */
61 : struct TALER_EXCHANGEDB_KycStatus kyc;
62 :
63 : /**
64 : * Hash of the payto-URI representing the reserve
65 : * from which we are withdrawing.
66 : */
67 : struct TALER_PaytoHashP h_payto;
68 :
69 : /**
70 : * Current time for the DB transaction.
71 : */
72 : struct GNUNET_TIME_Timestamp now;
73 :
74 : };
75 :
76 :
77 : /**
78 : * Function called to iterate over KYC-relevant
79 : * transaction amounts for a particular time range.
80 : * Called within a database transaction, so must
81 : * not start a new one.
82 : *
83 : * @param cls closure, identifies the event type and
84 : * account to iterate over events for
85 : * @param limit maximum time-range for which events
86 : * should be fetched (timestamp in the past)
87 : * @param cb function to call on each event found,
88 : * events must be returned in reverse chronological
89 : * order
90 : * @param cb_cls closure for @a cb
91 : */
92 : static void
93 0 : withdraw_amount_cb (void *cls,
94 : struct GNUNET_TIME_Absolute limit,
95 : TALER_EXCHANGEDB_KycAmountCallback cb,
96 : void *cb_cls)
97 : {
98 0 : struct WithdrawContext *wc = cls;
99 : enum GNUNET_DB_QueryStatus qs;
100 :
101 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
102 : "Signaling amount %s for KYC check\n",
103 : TALER_amount2s (&wc->collectable.amount_with_fee));
104 0 : if (GNUNET_OK !=
105 0 : cb (cb_cls,
106 0 : &wc->collectable.amount_with_fee,
107 : wc->now.abs_time))
108 0 : return;
109 0 : qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (
110 0 : TEH_plugin->cls,
111 0 : &wc->h_payto,
112 : limit,
113 : cb,
114 : cb_cls);
115 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
116 : "Got %d additional transactions for this withdrawal and limit %llu\n",
117 : qs,
118 : (unsigned long long) limit.abs_value_us);
119 0 : GNUNET_break (qs >= 0);
120 : }
121 :
122 :
123 : /**
124 : * Function implementing withdraw transaction. Runs the
125 : * transaction logic; IF it returns a non-error code, the transaction
126 : * logic MUST NOT queue a MHD response. IF it returns an hard error,
127 : * the transaction logic MUST queue a MHD response and set @a mhd_ret.
128 : * IF it returns the soft error code, the function MAY be called again
129 : * to retry and MUST not queue a MHD response.
130 : *
131 : * Note that "wc->collectable.sig" is set before entering this function as we
132 : * signed before entering the transaction.
133 : *
134 : * @param cls a `struct WithdrawContext *`
135 : * @param connection MHD request which triggered the transaction
136 : * @param[out] mhd_ret set to MHD response status for @a connection,
137 : * if transaction failed (!)
138 : * @return transaction status
139 : */
140 : static enum GNUNET_DB_QueryStatus
141 0 : withdraw_transaction (void *cls,
142 : struct MHD_Connection *connection,
143 : MHD_RESULT *mhd_ret)
144 : {
145 0 : struct WithdrawContext *wc = cls;
146 : enum GNUNET_DB_QueryStatus qs;
147 0 : bool found = false;
148 0 : bool balance_ok = false;
149 0 : bool nonce_ok = false;
150 : uint64_t ruuid;
151 : const struct TALER_CsNonce *nonce;
152 : const struct TALER_BlindedPlanchet *bp;
153 :
154 0 : wc->now = GNUNET_TIME_timestamp_get ();
155 0 : qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls,
156 0 : &wc->collectable.reserve_pub,
157 : &wc->h_payto);
158 0 : if (qs < 0)
159 0 : return qs;
160 : /* If no results, reserve was created by merge,
161 : in which case no KYC check is required as the
162 : merge already did that. */
163 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
164 : {
165 : const char *kyc_required;
166 :
167 0 : kyc_required = TALER_KYCLOGIC_kyc_test_required (
168 : TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW,
169 0 : &wc->h_payto,
170 0 : TEH_plugin->select_satisfied_kyc_processes,
171 0 : TEH_plugin->cls,
172 : &withdraw_amount_cb,
173 : wc);
174 0 : if (NULL != kyc_required)
175 : {
176 : /* insert KYC requirement into DB! */
177 0 : wc->kyc.ok = false;
178 0 : return TEH_plugin->insert_kyc_requirement_for_account (
179 0 : TEH_plugin->cls,
180 : kyc_required,
181 0 : &wc->h_payto,
182 : &wc->kyc.requirement_row);
183 : }
184 : }
185 0 : wc->kyc.ok = true;
186 0 : bp = &wc->blinded_planchet;
187 0 : nonce = (TALER_DENOMINATION_CS == bp->cipher)
188 : ? &bp->details.cs_blinded_planchet.nonce
189 0 : : NULL;
190 0 : qs = TEH_plugin->do_withdraw (TEH_plugin->cls,
191 : nonce,
192 0 : &wc->collectable,
193 : wc->now,
194 : &found,
195 : &balance_ok,
196 : &nonce_ok,
197 : &ruuid);
198 0 : if (0 > qs)
199 : {
200 0 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
201 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
202 : MHD_HTTP_INTERNAL_SERVER_ERROR,
203 : TALER_EC_GENERIC_DB_FETCH_FAILED,
204 : "do_withdraw");
205 0 : return qs;
206 : }
207 0 : if (! found)
208 : {
209 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
210 : MHD_HTTP_NOT_FOUND,
211 : TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
212 : NULL);
213 0 : return GNUNET_DB_STATUS_HARD_ERROR;
214 : }
215 0 : if (! balance_ok)
216 : {
217 0 : TEH_plugin->rollback (TEH_plugin->cls);
218 0 : *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance (
219 : connection,
220 0 : &wc->collectable.amount_with_fee,
221 0 : &wc->collectable.reserve_pub);
222 0 : return GNUNET_DB_STATUS_HARD_ERROR;
223 : }
224 0 : if (! nonce_ok)
225 : {
226 0 : TEH_plugin->rollback (TEH_plugin->cls);
227 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
228 : MHD_HTTP_CONFLICT,
229 : TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE,
230 : NULL);
231 0 : return GNUNET_DB_STATUS_HARD_ERROR;
232 : }
233 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
234 0 : TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++;
235 0 : return qs;
236 : }
237 :
238 :
239 : /**
240 : * Check if the @a rc is replayed and we already have an
241 : * answer. If so, replay the existing answer and return the
242 : * HTTP response.
243 : *
244 : * @param rc request context
245 : * @param[in,out] wc parsed request data
246 : * @param[out] mret HTTP status, set if we return true
247 : * @return true if the request is idempotent with an existing request
248 : * false if we did not find the request in the DB and did not set @a mret
249 : */
250 : static bool
251 0 : check_request_idempotent (struct TEH_RequestContext *rc,
252 : struct WithdrawContext *wc,
253 : MHD_RESULT *mret)
254 : {
255 : enum GNUNET_DB_QueryStatus qs;
256 :
257 0 : qs = TEH_plugin->get_withdraw_info (TEH_plugin->cls,
258 0 : &wc->h_coin_envelope,
259 : &wc->collectable);
260 0 : if (0 > qs)
261 : {
262 0 : GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
263 0 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
264 0 : *mret = TALER_MHD_reply_with_error (rc->connection,
265 : MHD_HTTP_INTERNAL_SERVER_ERROR,
266 : TALER_EC_GENERIC_DB_FETCH_FAILED,
267 : "get_withdraw_info");
268 0 : return true; /* well, kind-of */
269 : }
270 0 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
271 0 : return false;
272 : /* generate idempotent reply */
273 0 : TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW]++;
274 0 : *mret = TALER_MHD_REPLY_JSON_PACK (
275 : rc->connection,
276 : MHD_HTTP_OK,
277 : TALER_JSON_pack_blinded_denom_sig ("ev_sig",
278 : &wc->collectable.sig));
279 0 : TALER_blinded_denom_sig_free (&wc->collectable.sig);
280 0 : return true;
281 : }
282 :
283 :
284 : MHD_RESULT
285 0 : TEH_handler_withdraw (struct TEH_RequestContext *rc,
286 : const struct TALER_ReservePublicKeyP *reserve_pub,
287 : const json_t *root)
288 : {
289 : struct WithdrawContext wc;
290 : struct GNUNET_JSON_Specification spec[] = {
291 0 : GNUNET_JSON_spec_fixed_auto ("reserve_sig",
292 : &wc.collectable.reserve_sig),
293 0 : GNUNET_JSON_spec_fixed_auto ("denom_pub_hash",
294 : &wc.collectable.denom_pub_hash),
295 0 : TALER_JSON_spec_blinded_planchet ("coin_ev",
296 : &wc.blinded_planchet),
297 0 : GNUNET_JSON_spec_end ()
298 : };
299 : enum TALER_ErrorCode ec;
300 : struct TEH_DenominationKey *dk;
301 :
302 0 : memset (&wc,
303 : 0,
304 : sizeof (wc));
305 0 : wc.collectable.reserve_pub = *reserve_pub;
306 : {
307 : enum GNUNET_GenericReturnValue res;
308 :
309 0 : res = TALER_MHD_parse_json_data (rc->connection,
310 : root,
311 : spec);
312 0 : if (GNUNET_OK != res)
313 0 : return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES;
314 : }
315 : {
316 : MHD_RESULT mret;
317 : struct TEH_KeyStateHandle *ksh;
318 :
319 0 : ksh = TEH_keys_get_state ();
320 0 : if (NULL == ksh)
321 : {
322 0 : if (! check_request_idempotent (rc,
323 : &wc,
324 : &mret))
325 : {
326 0 : GNUNET_JSON_parse_free (spec);
327 0 : return TALER_MHD_reply_with_error (rc->connection,
328 : MHD_HTTP_INTERNAL_SERVER_ERROR,
329 : TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
330 : NULL);
331 : }
332 0 : GNUNET_JSON_parse_free (spec);
333 0 : return mret;
334 : }
335 0 : dk = TEH_keys_denomination_by_hash2 (ksh,
336 : &wc.collectable.denom_pub_hash,
337 : NULL,
338 : NULL);
339 0 : if (NULL == dk)
340 : {
341 0 : if (! check_request_idempotent (rc,
342 : &wc,
343 : &mret))
344 : {
345 0 : GNUNET_JSON_parse_free (spec);
346 0 : return TEH_RESPONSE_reply_unknown_denom_pub_hash (
347 : rc->connection,
348 : &wc.collectable.denom_pub_hash);
349 : }
350 0 : GNUNET_JSON_parse_free (spec);
351 0 : return mret;
352 : }
353 0 : if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time))
354 : {
355 : /* This denomination is past the expiration time for withdraws */
356 0 : if (! check_request_idempotent (rc,
357 : &wc,
358 : &mret))
359 : {
360 0 : GNUNET_JSON_parse_free (spec);
361 0 : return TEH_RESPONSE_reply_expired_denom_pub_hash (
362 : rc->connection,
363 : &wc.collectable.denom_pub_hash,
364 : TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
365 : "WITHDRAW");
366 : }
367 0 : GNUNET_JSON_parse_free (spec);
368 0 : return mret;
369 : }
370 0 : if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time))
371 : {
372 : /* This denomination is not yet valid, no need to check
373 : for idempotency! */
374 0 : GNUNET_JSON_parse_free (spec);
375 0 : return TEH_RESPONSE_reply_expired_denom_pub_hash (
376 : rc->connection,
377 : &wc.collectable.denom_pub_hash,
378 : TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
379 : "WITHDRAW");
380 : }
381 0 : if (dk->recoup_possible)
382 : {
383 : /* This denomination has been revoked */
384 0 : if (! check_request_idempotent (rc,
385 : &wc,
386 : &mret))
387 : {
388 0 : GNUNET_JSON_parse_free (spec);
389 0 : return TEH_RESPONSE_reply_expired_denom_pub_hash (
390 : rc->connection,
391 : &wc.collectable.denom_pub_hash,
392 : TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
393 : "WITHDRAW");
394 : }
395 0 : GNUNET_JSON_parse_free (spec);
396 0 : return mret;
397 : }
398 0 : if (dk->denom_pub.cipher != wc.blinded_planchet.cipher)
399 : {
400 : /* denomination cipher and blinded planchet cipher not the same */
401 0 : GNUNET_JSON_parse_free (spec);
402 0 : return TALER_MHD_reply_with_error (rc->connection,
403 : MHD_HTTP_BAD_REQUEST,
404 : TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
405 : NULL);
406 : }
407 : }
408 :
409 0 : if (0 >
410 0 : TALER_amount_add (&wc.collectable.amount_with_fee,
411 0 : &dk->meta.value,
412 0 : &dk->meta.fees.withdraw))
413 : {
414 0 : GNUNET_JSON_parse_free (spec);
415 0 : return TALER_MHD_reply_with_error (rc->connection,
416 : MHD_HTTP_INTERNAL_SERVER_ERROR,
417 : TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
418 : NULL);
419 : }
420 :
421 0 : if (GNUNET_OK !=
422 0 : TALER_coin_ev_hash (&wc.blinded_planchet,
423 : &wc.collectable.denom_pub_hash,
424 : &wc.collectable.h_coin_envelope))
425 : {
426 0 : GNUNET_break (0);
427 0 : GNUNET_JSON_parse_free (spec);
428 0 : return TALER_MHD_reply_with_error (rc->connection,
429 : MHD_HTTP_INTERNAL_SERVER_ERROR,
430 : TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
431 : NULL);
432 : }
433 :
434 0 : TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
435 0 : if (GNUNET_OK !=
436 0 : TALER_wallet_withdraw_verify (&wc.collectable.denom_pub_hash,
437 : &wc.collectable.amount_with_fee,
438 : &wc.collectable.h_coin_envelope,
439 : &wc.collectable.reserve_pub,
440 : &wc.collectable.reserve_sig))
441 : {
442 0 : GNUNET_break_op (0);
443 0 : GNUNET_JSON_parse_free (spec);
444 0 : return TALER_MHD_reply_with_error (rc->connection,
445 : MHD_HTTP_FORBIDDEN,
446 : TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
447 : NULL);
448 : }
449 :
450 : /* Sign before transaction! */
451 0 : ec = TEH_keys_denomination_sign_withdraw (
452 : &wc.collectable.denom_pub_hash,
453 : &wc.blinded_planchet,
454 : &wc.collectable.sig);
455 0 : if (TALER_EC_NONE != ec)
456 : {
457 0 : GNUNET_break (0);
458 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
459 : "Failed to sign coin: %d\n",
460 : ec);
461 0 : GNUNET_JSON_parse_free (spec);
462 0 : return TALER_MHD_reply_with_ec (rc->connection,
463 : ec,
464 : NULL);
465 : }
466 :
467 : /* run transaction */
468 : {
469 : MHD_RESULT mhd_ret;
470 :
471 0 : if (GNUNET_OK !=
472 0 : TEH_DB_run_transaction (rc->connection,
473 : "run withdraw",
474 : TEH_MT_REQUEST_WITHDRAW,
475 : &mhd_ret,
476 : &withdraw_transaction,
477 : &wc))
478 : {
479 : /* Even if #withdraw_transaction() failed, it may have created a signature
480 : (or we might have done it optimistically above). */
481 0 : TALER_blinded_denom_sig_free (&wc.collectable.sig);
482 0 : GNUNET_JSON_parse_free (spec);
483 0 : return mhd_ret;
484 : }
485 : }
486 :
487 : /* Clean up and send back final response */
488 0 : GNUNET_JSON_parse_free (spec);
489 :
490 0 : if (! wc.kyc.ok)
491 0 : return TEH_RESPONSE_reply_kyc_required (rc->connection,
492 : &wc.h_payto,
493 : &wc.kyc);
494 : {
495 : MHD_RESULT ret;
496 :
497 0 : ret = TALER_MHD_REPLY_JSON_PACK (
498 : rc->connection,
499 : MHD_HTTP_OK,
500 : TALER_JSON_pack_blinded_denom_sig ("ev_sig",
501 : &wc.collectable.sig));
502 0 : TALER_blinded_denom_sig_free (&wc.collectable.sig);
503 0 : return ret;
504 : }
505 : }
506 :
507 :
508 : /* end of taler-exchange-httpd_withdraw.c */
|