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 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 General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see
15 : <http://www.gnu.org/licenses/>
16 : */
17 : /**
18 : * @file lib/exchange_api_withdraw2.c
19 : * @brief Implementation of /reserves/$RESERVE_PUB/withdraw requests without blinding/unblinding
20 : * @author Christian Grothoff
21 : */
22 : #include "platform.h"
23 : #include <jansson.h>
24 : #include <microhttpd.h> /* just for HTTP status codes */
25 : #include <gnunet/gnunet_util_lib.h>
26 : #include <gnunet/gnunet_json_lib.h>
27 : #include <gnunet/gnunet_curl_lib.h>
28 : #include "taler_exchange_service.h"
29 : #include "taler_json_lib.h"
30 : #include "exchange_api_handle.h"
31 : #include "taler_signatures.h"
32 : #include "exchange_api_curl_defaults.h"
33 :
34 :
35 : /**
36 : * @brief A Withdraw Handle
37 : */
38 : struct TALER_EXCHANGE_Withdraw2Handle
39 : {
40 :
41 : /**
42 : * The connection to exchange this request handle will use
43 : */
44 : struct TALER_EXCHANGE_Handle *exchange;
45 :
46 : /**
47 : * The url for this request.
48 : */
49 : char *url;
50 :
51 : /**
52 : * Handle for the request.
53 : */
54 : struct GNUNET_CURL_Job *job;
55 :
56 : /**
57 : * Function to call with the result.
58 : */
59 : TALER_EXCHANGE_Withdraw2Callback cb;
60 :
61 : /**
62 : * Closure for @a cb.
63 : */
64 : void *cb_cls;
65 :
66 : /**
67 : * Context for #TEH_curl_easy_post(). Keeps the data that must
68 : * persist for Curl to make the upload.
69 : */
70 : struct TALER_CURL_PostContext post_ctx;
71 :
72 : /**
73 : * Total amount requested (value plus withdraw fee).
74 : */
75 : struct TALER_Amount requested_amount;
76 :
77 : /**
78 : * Public key of the reserve we are withdrawing from.
79 : */
80 : struct TALER_ReservePublicKeyP reserve_pub;
81 :
82 : };
83 :
84 :
85 : /**
86 : * We got a 200 OK response for the /reserves/$RESERVE_PUB/withdraw operation.
87 : * Extract the coin's signature and return it to the caller. The signature we
88 : * get from the exchange is for the blinded value. Thus, we first must
89 : * unblind it and then should verify its validity against our coin's hash.
90 : *
91 : * If everything checks out, we return the unblinded signature
92 : * to the application via the callback.
93 : *
94 : * @param wh operation handle
95 : * @param json reply from the exchange
96 : * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
97 : */
98 : static enum GNUNET_GenericReturnValue
99 0 : reserve_withdraw_ok (struct TALER_EXCHANGE_Withdraw2Handle *wh,
100 : const json_t *json)
101 : {
102 : struct TALER_BlindedDenominationSignature blind_sig;
103 : struct GNUNET_JSON_Specification spec[] = {
104 0 : TALER_JSON_spec_blinded_denom_sig ("ev_sig",
105 : &blind_sig),
106 0 : GNUNET_JSON_spec_end ()
107 : };
108 0 : struct TALER_EXCHANGE_HttpResponse hr = {
109 : .reply = json,
110 : .http_status = MHD_HTTP_OK
111 : };
112 :
113 0 : if (GNUNET_OK !=
114 0 : GNUNET_JSON_parse (json,
115 : spec,
116 : NULL, NULL))
117 : {
118 0 : GNUNET_break_op (0);
119 0 : return GNUNET_SYSERR;
120 : }
121 :
122 : /* signature is valid, return it to the application */
123 0 : wh->cb (wh->cb_cls,
124 : &hr,
125 : &blind_sig);
126 : /* make sure callback isn't called again after return */
127 0 : wh->cb = NULL;
128 0 : GNUNET_JSON_parse_free (spec);
129 0 : return GNUNET_OK;
130 : }
131 :
132 :
133 : /**
134 : * We got a 409 CONFLICT response for the /reserves/$RESERVE_PUB/withdraw operation.
135 : * Check the signatures on the withdraw transactions in the provided
136 : * history and that the balances add up. We don't do anything directly
137 : * with the information, as the JSON will be returned to the application.
138 : * However, our job is ensuring that the exchange followed the protocol, and
139 : * this in particular means checking all of the signatures in the history.
140 : *
141 : * @param wh operation handle
142 : * @param json reply from the exchange
143 : * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
144 : */
145 : static enum GNUNET_GenericReturnValue
146 0 : reserve_withdraw_payment_required (
147 : struct TALER_EXCHANGE_Withdraw2Handle *wh,
148 : const json_t *json)
149 : {
150 : struct TALER_Amount balance;
151 : struct TALER_Amount total_in_from_history;
152 : struct TALER_Amount total_out_from_history;
153 : json_t *history;
154 : size_t len;
155 : struct GNUNET_JSON_Specification spec[] = {
156 0 : TALER_JSON_spec_amount_any ("balance",
157 : &balance),
158 0 : GNUNET_JSON_spec_end ()
159 : };
160 :
161 0 : if (GNUNET_OK !=
162 0 : GNUNET_JSON_parse (json,
163 : spec,
164 : NULL, NULL))
165 : {
166 0 : GNUNET_break_op (0);
167 0 : return GNUNET_SYSERR;
168 : }
169 0 : history = json_object_get (json,
170 : "history");
171 0 : if (NULL == history)
172 : {
173 0 : GNUNET_break_op (0);
174 0 : return GNUNET_SYSERR;
175 : }
176 :
177 : /* go over transaction history and compute
178 : total incoming and outgoing amounts */
179 0 : len = json_array_size (history);
180 : {
181 : struct TALER_EXCHANGE_ReserveHistoryEntry *rhistory;
182 :
183 : /* Use heap allocation as "len" may be very big and thus this may
184 : not fit on the stack. Use "GNUNET_malloc_large" as a malicious
185 : exchange may theoretically try to crash us by giving a history
186 : that does not fit into our memory. */
187 0 : rhistory = GNUNET_malloc_large (
188 : sizeof (struct TALER_EXCHANGE_ReserveHistoryEntry)
189 : * len);
190 0 : if (NULL == rhistory)
191 : {
192 0 : GNUNET_break (0);
193 0 : return GNUNET_SYSERR;
194 : }
195 :
196 0 : if (GNUNET_OK !=
197 0 : TALER_EXCHANGE_parse_reserve_history (wh->exchange,
198 : history,
199 0 : &wh->reserve_pub,
200 : balance.currency,
201 : &total_in_from_history,
202 : &total_out_from_history,
203 : len,
204 : rhistory))
205 : {
206 0 : GNUNET_break_op (0);
207 0 : TALER_EXCHANGE_free_reserve_history (rhistory,
208 : len);
209 0 : return GNUNET_SYSERR;
210 : }
211 0 : TALER_EXCHANGE_free_reserve_history (rhistory,
212 : len);
213 : }
214 :
215 : /* Check that funds were really insufficient */
216 0 : if (0 >= TALER_amount_cmp (&wh->requested_amount,
217 : &balance))
218 : {
219 : /* Requested amount is smaller or equal to reported balance,
220 : so this should not have failed. */
221 0 : GNUNET_break_op (0);
222 0 : return GNUNET_SYSERR;
223 : }
224 0 : return GNUNET_OK;
225 : }
226 :
227 :
228 : /**
229 : * Function called when we're done processing the
230 : * HTTP /reserves/$RESERVE_PUB/withdraw request.
231 : *
232 : * @param cls the `struct TALER_EXCHANGE_WithdrawHandle`
233 : * @param response_code HTTP response code, 0 on error
234 : * @param response parsed JSON result, NULL on error
235 : */
236 : static void
237 0 : handle_reserve_withdraw_finished (void *cls,
238 : long response_code,
239 : const void *response)
240 : {
241 0 : struct TALER_EXCHANGE_Withdraw2Handle *wh = cls;
242 0 : const json_t *j = response;
243 0 : struct TALER_EXCHANGE_HttpResponse hr = {
244 : .reply = j,
245 0 : .http_status = (unsigned int) response_code
246 : };
247 :
248 0 : wh->job = NULL;
249 0 : switch (response_code)
250 : {
251 0 : case 0:
252 0 : hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
253 0 : break;
254 0 : case MHD_HTTP_OK:
255 0 : if (GNUNET_OK !=
256 0 : reserve_withdraw_ok (wh,
257 : j))
258 : {
259 0 : GNUNET_break_op (0);
260 0 : hr.http_status = 0;
261 0 : hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
262 0 : break;
263 : }
264 0 : GNUNET_assert (NULL == wh->cb);
265 0 : TALER_EXCHANGE_withdraw2_cancel (wh);
266 0 : return;
267 0 : case MHD_HTTP_BAD_REQUEST:
268 : /* This should never happen, either us or the exchange is buggy
269 : (or API version conflict); just pass JSON reply to the application */
270 0 : hr.ec = TALER_JSON_get_error_code (j);
271 0 : hr.hint = TALER_JSON_get_error_hint (j);
272 0 : break;
273 0 : case MHD_HTTP_FORBIDDEN:
274 0 : GNUNET_break_op (0);
275 : /* Nothing really to verify, exchange says one of the signatures is
276 : invalid; as we checked them, this should never happen, we
277 : should pass the JSON reply to the application */
278 0 : hr.ec = TALER_JSON_get_error_code (j);
279 0 : hr.hint = TALER_JSON_get_error_hint (j);
280 0 : break;
281 0 : case MHD_HTTP_NOT_FOUND:
282 : /* Nothing really to verify, the exchange basically just says
283 : that it doesn't know this reserve. Can happen if we
284 : query before the wire transfer went through.
285 : We should simply pass the JSON reply to the application. */
286 0 : hr.ec = TALER_JSON_get_error_code (j);
287 0 : hr.hint = TALER_JSON_get_error_hint (j);
288 0 : break;
289 0 : case MHD_HTTP_CONFLICT:
290 : /* The exchange says that the reserve has insufficient funds;
291 : check the signatures in the history... */
292 0 : if (GNUNET_OK !=
293 0 : reserve_withdraw_payment_required (wh,
294 : j))
295 : {
296 0 : GNUNET_break_op (0);
297 0 : hr.http_status = 0;
298 0 : hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
299 : }
300 : else
301 : {
302 0 : hr.ec = TALER_JSON_get_error_code (j);
303 0 : hr.hint = TALER_JSON_get_error_hint (j);
304 : }
305 0 : break;
306 0 : case MHD_HTTP_GONE:
307 : /* could happen if denomination was revoked */
308 : /* Note: one might want to check /keys for revocation
309 : signature here, alas tricky in case our /keys
310 : is outdated => left to clients */
311 0 : hr.ec = TALER_JSON_get_error_code (j);
312 0 : hr.hint = TALER_JSON_get_error_hint (j);
313 0 : break;
314 0 : case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
315 : /* only validate reply is well-formed */
316 : {
317 : uint64_t ptu;
318 : struct GNUNET_JSON_Specification spec[] = {
319 0 : GNUNET_JSON_spec_uint64 ("requirement_row",
320 : &ptu),
321 0 : GNUNET_JSON_spec_end ()
322 : };
323 :
324 0 : if (GNUNET_OK !=
325 0 : GNUNET_JSON_parse (j,
326 : spec,
327 : NULL, NULL))
328 : {
329 0 : GNUNET_break_op (0);
330 0 : hr.http_status = 0;
331 0 : hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
332 0 : break;
333 : }
334 : }
335 0 : break;
336 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
337 : /* Server had an internal issue; we should retry, but this API
338 : leaves this to the application */
339 0 : hr.ec = TALER_JSON_get_error_code (j);
340 0 : hr.hint = TALER_JSON_get_error_hint (j);
341 0 : break;
342 0 : default:
343 : /* unexpected response code */
344 0 : GNUNET_break_op (0);
345 0 : hr.ec = TALER_JSON_get_error_code (j);
346 0 : hr.hint = TALER_JSON_get_error_hint (j);
347 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
348 : "Unexpected response code %u/%d for exchange withdraw\n",
349 : (unsigned int) response_code,
350 : (int) hr.ec);
351 0 : break;
352 : }
353 0 : if (NULL != wh->cb)
354 : {
355 0 : wh->cb (wh->cb_cls,
356 : &hr,
357 : NULL);
358 0 : wh->cb = NULL;
359 : }
360 0 : TALER_EXCHANGE_withdraw2_cancel (wh);
361 : }
362 :
363 :
364 : struct TALER_EXCHANGE_Withdraw2Handle *
365 0 : TALER_EXCHANGE_withdraw2 (
366 : struct TALER_EXCHANGE_Handle *exchange,
367 : const struct TALER_PlanchetDetail *pd,
368 : const struct TALER_ReservePrivateKeyP *reserve_priv,
369 : TALER_EXCHANGE_Withdraw2Callback res_cb,
370 : void *res_cb_cls)
371 : {
372 : struct TALER_EXCHANGE_Withdraw2Handle *wh;
373 : const struct TALER_EXCHANGE_Keys *keys;
374 : const struct TALER_EXCHANGE_DenomPublicKey *dk;
375 : struct TALER_ReserveSignatureP reserve_sig;
376 : char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
377 : struct TALER_BlindedCoinHashP bch;
378 :
379 0 : keys = TALER_EXCHANGE_get_keys (exchange);
380 0 : if (NULL == keys)
381 : {
382 0 : GNUNET_break (0);
383 0 : return NULL;
384 : }
385 0 : dk = TALER_EXCHANGE_get_denomination_key_by_hash (keys,
386 : &pd->denom_pub_hash);
387 0 : if (NULL == dk)
388 : {
389 0 : GNUNET_break (0);
390 0 : return NULL;
391 : }
392 0 : wh = GNUNET_new (struct TALER_EXCHANGE_Withdraw2Handle);
393 0 : wh->exchange = exchange;
394 0 : wh->cb = res_cb;
395 0 : wh->cb_cls = res_cb_cls;
396 : /* Compute how much we expected to charge to the reserve */
397 0 : if (0 >
398 0 : TALER_amount_add (&wh->requested_amount,
399 : &dk->value,
400 : &dk->fees.withdraw))
401 : {
402 : /* Overflow here? Very strange, our CPU must be fried... */
403 0 : GNUNET_break (0);
404 0 : GNUNET_free (wh);
405 0 : return NULL;
406 : }
407 :
408 0 : GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
409 : &wh->reserve_pub.eddsa_pub);
410 :
411 : {
412 : char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2];
413 : char *end;
414 :
415 0 : end = GNUNET_STRINGS_data_to_string (
416 0 : &wh->reserve_pub,
417 : sizeof (struct TALER_ReservePublicKeyP),
418 : pub_str,
419 : sizeof (pub_str));
420 0 : *end = '\0';
421 0 : GNUNET_snprintf (arg_str,
422 : sizeof (arg_str),
423 : "/reserves/%s/withdraw",
424 : pub_str);
425 : }
426 :
427 0 : if (GNUNET_OK !=
428 0 : TALER_coin_ev_hash (&pd->blinded_planchet,
429 : &pd->denom_pub_hash,
430 : &bch))
431 : {
432 0 : GNUNET_break (0);
433 0 : GNUNET_free (wh);
434 0 : return NULL;
435 : }
436 :
437 0 : TALER_wallet_withdraw_sign (&pd->denom_pub_hash,
438 0 : &wh->requested_amount,
439 : &bch,
440 : reserve_priv,
441 : &reserve_sig);
442 : {
443 0 : json_t *withdraw_obj = GNUNET_JSON_PACK (
444 : GNUNET_JSON_pack_data_auto ("denom_pub_hash",
445 : &pd->denom_pub_hash),
446 : TALER_JSON_pack_blinded_planchet ("coin_ev",
447 : &pd->blinded_planchet),
448 : GNUNET_JSON_pack_data_auto ("reserve_sig",
449 : &reserve_sig));
450 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
451 : "Attempting to withdraw from reserve %s\n",
452 : TALER_B2S (&wh->reserve_pub));
453 0 : wh->url = TEAH_path_to_url (exchange,
454 : arg_str);
455 0 : if (NULL == wh->url)
456 : {
457 0 : json_decref (withdraw_obj);
458 0 : GNUNET_free (wh);
459 0 : return NULL;
460 : }
461 : {
462 : CURL *eh;
463 : struct GNUNET_CURL_Context *ctx;
464 :
465 0 : ctx = TEAH_handle_to_context (exchange);
466 0 : eh = TALER_EXCHANGE_curl_easy_get_ (wh->url);
467 0 : if ( (NULL == eh) ||
468 : (GNUNET_OK !=
469 0 : TALER_curl_easy_post (&wh->post_ctx,
470 : eh,
471 : withdraw_obj)) )
472 : {
473 0 : GNUNET_break (0);
474 0 : if (NULL != eh)
475 0 : curl_easy_cleanup (eh);
476 0 : json_decref (withdraw_obj);
477 0 : GNUNET_free (wh->url);
478 0 : GNUNET_free (wh);
479 0 : return NULL;
480 : }
481 0 : json_decref (withdraw_obj);
482 0 : wh->job = GNUNET_CURL_job_add2 (ctx,
483 : eh,
484 0 : wh->post_ctx.headers,
485 : &handle_reserve_withdraw_finished,
486 : wh);
487 : }
488 : }
489 0 : return wh;
490 : }
491 :
492 :
493 : void
494 0 : TALER_EXCHANGE_withdraw2_cancel (struct TALER_EXCHANGE_Withdraw2Handle *wh)
495 : {
496 0 : if (NULL != wh->job)
497 : {
498 0 : GNUNET_CURL_job_cancel (wh->job);
499 0 : wh->job = NULL;
500 : }
501 0 : GNUNET_free (wh->url);
502 0 : TALER_curl_easy_post_finished (&wh->post_ctx);
503 0 : GNUNET_free (wh);
504 0 : }
|