Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2014-2026 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_post-reserves-RESERVE_PUB-open.c
19 : * @brief Implementation of the POST /reserves/$RESERVE_PUB/open requests
20 : * @author Christian Grothoff
21 : */
22 : #include <jansson.h>
23 : #include <microhttpd.h> /* just for HTTP open codes */
24 : #include <gnunet/gnunet_util_lib.h>
25 : #include <gnunet/gnunet_json_lib.h>
26 : #include <gnunet/gnunet_curl_lib.h>
27 : #include "taler/taler_json_lib.h"
28 : #include "exchange_api_common.h"
29 : #include "exchange_api_handle.h"
30 : #include "taler/taler_signatures.h"
31 : #include "exchange_api_curl_defaults.h"
32 :
33 :
34 : /**
35 : * Information we keep per coin to validate the reply.
36 : */
37 : struct CoinData
38 : {
39 : /**
40 : * Public key of the coin.
41 : */
42 : struct TALER_CoinSpendPublicKeyP coin_pub;
43 :
44 : /**
45 : * Signature by the coin.
46 : */
47 : struct TALER_CoinSpendSignatureP coin_sig;
48 :
49 : /**
50 : * The hash of the denomination's public key
51 : */
52 : struct TALER_DenominationHashP h_denom_pub;
53 :
54 : /**
55 : * Hash of the age commitment of the coin, if @e have_age.
56 : */
57 : struct TALER_AgeCommitmentHashP h_age_commitment;
58 :
59 : /**
60 : * True if the coin has an age commitment.
61 : */
62 : bool have_age;
63 :
64 : /**
65 : * How much did this coin contribute.
66 : */
67 : struct TALER_Amount contribution;
68 : };
69 :
70 :
71 : /**
72 : * @brief A POST /reserves/$RID/open Handle
73 : */
74 : struct TALER_EXCHANGE_PostReservesOpenHandle
75 : {
76 :
77 : /**
78 : * Reference to the execution context.
79 : */
80 : struct GNUNET_CURL_Context *ctx;
81 :
82 : /**
83 : * Base URL of the exchange.
84 : */
85 : char *base_url;
86 :
87 : /**
88 : * The url for this request, set during _start.
89 : */
90 : char *url;
91 :
92 : /**
93 : * Context for #TEH_curl_easy_post(). Keeps the data that must
94 : * persist for Curl to make the upload.
95 : */
96 : struct TALER_CURL_PostContext post_ctx;
97 :
98 : /**
99 : * Handle for the request.
100 : */
101 : struct GNUNET_CURL_Job *job;
102 :
103 : /**
104 : * Function to call with the result.
105 : */
106 : TALER_EXCHANGE_PostReservesOpenCallback cb;
107 :
108 : /**
109 : * Closure for @a cb.
110 : */
111 : TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls;
112 :
113 : /**
114 : * The keys of the exchange this request handle will use
115 : */
116 : struct TALER_EXCHANGE_Keys *keys;
117 :
118 : /**
119 : * Public key of the reserve we are querying.
120 : */
121 : struct TALER_ReservePublicKeyP reserve_pub;
122 :
123 : /**
124 : * Information we keep per coin to validate the reply.
125 : */
126 : struct CoinData *coins;
127 :
128 : /**
129 : * Length of the @e coins array.
130 : */
131 : unsigned int num_coins;
132 :
133 : /**
134 : * Pre-built JSON body for the request.
135 : */
136 : json_t *body;
137 :
138 : };
139 :
140 :
141 : /**
142 : * We received an #MHD_HTTP_OK open code. Handle the JSON
143 : * response.
144 : *
145 : * @param proh handle of the request
146 : * @param j JSON response
147 : * @return #GNUNET_OK on success
148 : */
149 : static enum GNUNET_GenericReturnValue
150 4 : handle_reserves_open_ok (struct TALER_EXCHANGE_PostReservesOpenHandle *proh,
151 : const json_t *j)
152 : {
153 4 : struct TALER_EXCHANGE_PostReservesOpenResponse rs = {
154 : .hr.reply = j,
155 : .hr.http_status = MHD_HTTP_OK,
156 : };
157 : struct GNUNET_JSON_Specification spec[] = {
158 4 : TALER_JSON_spec_amount_any ("open_cost",
159 : &rs.details.ok.open_cost),
160 4 : GNUNET_JSON_spec_timestamp ("reserve_expiration",
161 : &rs.details.ok.expiration_time),
162 4 : GNUNET_JSON_spec_end ()
163 : };
164 :
165 4 : if (GNUNET_OK !=
166 4 : GNUNET_JSON_parse (j,
167 : spec,
168 : NULL,
169 : NULL))
170 : {
171 0 : GNUNET_break_op (0);
172 0 : return GNUNET_SYSERR;
173 : }
174 4 : proh->cb (proh->cb_cls,
175 : &rs);
176 4 : proh->cb = NULL;
177 4 : GNUNET_JSON_parse_free (spec);
178 4 : return GNUNET_OK;
179 : }
180 :
181 :
182 : /**
183 : * We received an #MHD_HTTP_PAYMENT_REQUIRED open code. Handle the JSON
184 : * response.
185 : *
186 : * @param proh handle of the request
187 : * @param j JSON response
188 : * @return #GNUNET_OK on success
189 : */
190 : static enum GNUNET_GenericReturnValue
191 2 : handle_reserves_open_pr (struct TALER_EXCHANGE_PostReservesOpenHandle *proh,
192 : const json_t *j)
193 : {
194 2 : struct TALER_EXCHANGE_PostReservesOpenResponse rs = {
195 : .hr.reply = j,
196 : .hr.http_status = MHD_HTTP_PAYMENT_REQUIRED,
197 : };
198 : struct GNUNET_JSON_Specification spec[] = {
199 2 : TALER_JSON_spec_amount_any ("open_cost",
200 : &rs.details.payment_required.open_cost),
201 2 : GNUNET_JSON_spec_timestamp ("reserve_expiration",
202 : &rs.details.payment_required.expiration_time),
203 2 : GNUNET_JSON_spec_end ()
204 : };
205 :
206 2 : if (GNUNET_OK !=
207 2 : GNUNET_JSON_parse (j,
208 : spec,
209 : NULL,
210 : NULL))
211 : {
212 0 : GNUNET_break_op (0);
213 0 : return GNUNET_SYSERR;
214 : }
215 2 : proh->cb (proh->cb_cls,
216 : &rs);
217 2 : proh->cb = NULL;
218 2 : GNUNET_JSON_parse_free (spec);
219 2 : return GNUNET_OK;
220 : }
221 :
222 :
223 : /**
224 : * Find the coin @a coin_pub paid into the reserve and how we used it.
225 : *
226 : * @param cls a `struct TALER_EXCHANGE_PostReservesOpenHandle *`
227 : * @param coin_pub public key of the coin named in the conflict reply
228 : * @param[out] h_denom_pub set to the hash of the denomination we used
229 : * @param[out] h_age_commitment set to the age commitment hash we used, or NULL
230 : * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours
231 : */
232 : static enum GNUNET_GenericReturnValue
233 0 : reserve_open_coin_lookup (void *cls,
234 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
235 : const struct TALER_DenominationHashP **h_denom_pub,
236 : const struct TALER_AgeCommitmentHashP **
237 : h_age_commitment)
238 : {
239 0 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh = cls;
240 :
241 0 : for (unsigned int i = 0; i < proh->num_coins; i++)
242 : {
243 0 : const struct CoinData *cd = &proh->coins[i];
244 :
245 0 : if (0 != GNUNET_memcmp (coin_pub,
246 : &cd->coin_pub))
247 0 : continue;
248 0 : *h_denom_pub = &cd->h_denom_pub;
249 0 : *h_age_commitment = cd->have_age
250 : ? &cd->h_age_commitment
251 0 : : NULL;
252 0 : return GNUNET_OK;
253 : }
254 0 : return GNUNET_NO;
255 : }
256 :
257 :
258 : /**
259 : * Function called when we're done processing the
260 : * HTTP /reserves/$RID/open request.
261 : *
262 : * @param cls the `struct TALER_EXCHANGE_PostReservesOpenHandle`
263 : * @param response_code HTTP response code, 0 on error
264 : * @param response parsed JSON result, NULL on error
265 : */
266 : static void
267 6 : handle_reserves_open_finished (void *cls,
268 : long response_code,
269 : const void *response)
270 : {
271 6 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh = cls;
272 6 : const json_t *j = response;
273 6 : struct TALER_EXCHANGE_PostReservesOpenResponse rs = {
274 : .hr.reply = j,
275 6 : .hr.http_status = (unsigned int) response_code
276 : };
277 :
278 6 : proh->job = NULL;
279 6 : switch (response_code)
280 : {
281 0 : case 0:
282 0 : rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
283 0 : break;
284 4 : case MHD_HTTP_OK:
285 4 : if (GNUNET_OK !=
286 4 : handle_reserves_open_ok (proh,
287 : j))
288 : {
289 0 : GNUNET_break_op (0);
290 0 : rs.hr.http_status = 0;
291 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
292 : }
293 4 : break;
294 0 : case MHD_HTTP_BAD_REQUEST:
295 : /* This should never happen, either us or the exchange is buggy
296 : (or API version conflict); just pass JSON reply to the application */
297 0 : GNUNET_break (0);
298 0 : json_dumpf (j,
299 : stderr,
300 : JSON_INDENT (2));
301 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
302 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
303 0 : break;
304 2 : case MHD_HTTP_PAYMENT_REQUIRED:
305 2 : if (GNUNET_OK !=
306 2 : handle_reserves_open_pr (proh,
307 : j))
308 : {
309 0 : GNUNET_break_op (0);
310 0 : rs.hr.http_status = 0;
311 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
312 : }
313 2 : break;
314 0 : case MHD_HTTP_FORBIDDEN:
315 : /* This should never happen, either us or the exchange is buggy
316 : (or API version conflict); just pass JSON reply to the application */
317 0 : GNUNET_break (0);
318 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
319 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
320 0 : break;
321 0 : case MHD_HTTP_NOT_FOUND:
322 : /* Nothing really to verify, this should never
323 : happen, we should pass the JSON reply to the application */
324 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
325 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
326 0 : break;
327 0 : case MHD_HTTP_CONFLICT:
328 : {
329 0 : const struct CoinData *cd = NULL;
330 : struct GNUNET_JSON_Specification spec[] = {
331 0 : GNUNET_JSON_spec_fixed_auto ("coin_pub",
332 : &rs.details.conflict.coin_pub),
333 0 : GNUNET_JSON_spec_end ()
334 : };
335 :
336 0 : if (GNUNET_OK !=
337 0 : GNUNET_JSON_parse (j,
338 : spec,
339 : NULL,
340 : NULL))
341 : {
342 0 : GNUNET_break_op (0);
343 0 : rs.hr.http_status = 0;
344 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
345 0 : break;
346 : }
347 0 : for (unsigned int i = 0; i < proh->num_coins; i++)
348 : {
349 0 : const struct CoinData *cdi = &proh->coins[i];
350 :
351 0 : if (0 == GNUNET_memcmp (&rs.details.conflict.coin_pub,
352 : &cdi->coin_pub))
353 : {
354 0 : cd = cdi;
355 0 : break;
356 : }
357 : }
358 0 : if (NULL == cd)
359 : {
360 0 : GNUNET_break_op (0);
361 0 : rs.hr.http_status = 0;
362 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
363 0 : break;
364 : }
365 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
366 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
367 0 : switch (rs.hr.ec)
368 : {
369 0 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
370 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH:
371 0 : if (GNUNET_OK !=
372 0 : TALER_EXCHANGE_check_coin_conflict_ (proh->keys,
373 : rs.hr.ec,
374 : j,
375 : &reserve_open_coin_lookup,
376 : proh,
377 : &rs.details.conflict.
378 : coin_conflict))
379 : {
380 0 : GNUNET_break_op (0);
381 0 : rs.hr.http_status = 0;
382 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
383 : }
384 0 : break;
385 0 : default:
386 : /* insufficient funds: proof is the coin history, checked by
387 : the application via GET /coins/$COIN_PUB/history */
388 0 : break;
389 : }
390 0 : break;
391 : }
392 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
393 : /* Server had an internal issue; we should retry, but this API
394 : leaves this to the application */
395 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
396 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
397 0 : break;
398 0 : default:
399 : /* unexpected response code */
400 0 : GNUNET_break_op (0);
401 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
402 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
403 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
404 : "Unexpected response code %u/%d for reserves open\n",
405 : (unsigned int) response_code,
406 : (int) rs.hr.ec);
407 0 : break;
408 : }
409 6 : if (NULL != proh->cb)
410 : {
411 0 : proh->cb (proh->cb_cls,
412 : &rs);
413 0 : proh->cb = NULL;
414 : }
415 6 : TALER_EXCHANGE_free_coin_conflict_ (&rs.hr,
416 : &rs.details.conflict.coin_conflict);
417 6 : TALER_EXCHANGE_post_reserves_open_cancel (proh);
418 6 : }
419 :
420 :
421 : struct TALER_EXCHANGE_PostReservesOpenHandle *
422 6 : TALER_EXCHANGE_post_reserves_open_create (
423 : struct GNUNET_CURL_Context *ctx,
424 : const char *url,
425 : struct TALER_EXCHANGE_Keys *keys,
426 : const struct TALER_ReservePrivateKeyP *reserve_priv,
427 : const struct TALER_Amount *reserve_contribution,
428 : unsigned int coin_payments_length,
429 : const struct TALER_EXCHANGE_PurseDeposit coin_payments[
430 : static coin_payments_length],
431 : struct GNUNET_TIME_Timestamp expiration_time,
432 : uint32_t min_purses)
433 6 : {
434 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh;
435 : struct GNUNET_TIME_Timestamp ts;
436 : struct TALER_ReserveSignatureP reserve_sig;
437 : json_t *cpa;
438 :
439 6 : proh = GNUNET_new (struct TALER_EXCHANGE_PostReservesOpenHandle);
440 6 : proh->ctx = ctx;
441 6 : proh->base_url = GNUNET_strdup (url);
442 6 : proh->keys = TALER_EXCHANGE_keys_incref (keys);
443 6 : ts = GNUNET_TIME_timestamp_get ();
444 6 : GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
445 : &proh->reserve_pub.eddsa_pub);
446 6 : TALER_wallet_reserve_open_sign (reserve_contribution,
447 : ts,
448 : expiration_time,
449 : min_purses,
450 : reserve_priv,
451 : &reserve_sig);
452 6 : proh->coins = GNUNET_new_array (coin_payments_length,
453 : struct CoinData);
454 6 : proh->num_coins = coin_payments_length;
455 6 : cpa = json_array ();
456 6 : GNUNET_assert (NULL != cpa);
457 8 : for (unsigned int i = 0; i < coin_payments_length; i++)
458 : {
459 2 : const struct TALER_EXCHANGE_PurseDeposit *pd = &coin_payments[i];
460 2 : const struct TALER_AgeCommitmentProof *acp = pd->age_commitment_proof;
461 2 : struct TALER_AgeCommitmentHashP ahac = { 0 };
462 2 : struct TALER_AgeCommitmentHashP *achp = NULL;
463 2 : struct CoinData *cd = &proh->coins[i];
464 : json_t *cp;
465 :
466 2 : cd->contribution = pd->amount;
467 2 : cd->h_denom_pub = pd->h_denom_pub;
468 2 : if (NULL != acp)
469 : {
470 0 : TALER_age_commitment_hash (&acp->commitment,
471 : &ahac);
472 0 : achp = &ahac;
473 0 : cd->h_age_commitment = ahac;
474 0 : cd->have_age = true;
475 : }
476 2 : TALER_wallet_reserve_open_deposit_sign (&pd->amount,
477 : &pd->h_denom_pub,
478 : achp,
479 : &reserve_sig,
480 : &pd->coin_priv,
481 : &cd->coin_sig);
482 2 : GNUNET_CRYPTO_eddsa_key_get_public (&pd->coin_priv.eddsa_priv,
483 : &cd->coin_pub.eddsa_pub);
484 :
485 2 : cp = GNUNET_JSON_PACK (
486 : GNUNET_JSON_pack_allow_null (
487 : GNUNET_JSON_pack_data_auto ("h_age_commitment",
488 : achp)),
489 : TALER_JSON_pack_amount ("amount",
490 : &pd->amount),
491 : GNUNET_JSON_pack_data_auto ("denom_pub_hash",
492 : &pd->h_denom_pub),
493 : TALER_JSON_pack_denom_sig ("ub_sig",
494 : &pd->denom_sig),
495 : GNUNET_JSON_pack_data_auto ("coin_pub",
496 : &cd->coin_pub),
497 : GNUNET_JSON_pack_data_auto ("coin_sig",
498 : &cd->coin_sig));
499 2 : GNUNET_assert (0 ==
500 : json_array_append_new (cpa,
501 : cp));
502 : }
503 6 : proh->body = GNUNET_JSON_PACK (
504 : GNUNET_JSON_pack_timestamp ("request_timestamp",
505 : ts),
506 : GNUNET_JSON_pack_timestamp ("reserve_expiration",
507 : expiration_time),
508 : GNUNET_JSON_pack_array_steal ("payments",
509 : cpa),
510 : TALER_JSON_pack_amount ("reserve_payment",
511 : reserve_contribution),
512 : GNUNET_JSON_pack_uint64 ("purse_limit",
513 : min_purses),
514 : GNUNET_JSON_pack_data_auto ("reserve_sig",
515 : &reserve_sig));
516 6 : if (NULL == proh->body)
517 : {
518 0 : GNUNET_break (0);
519 0 : GNUNET_free (proh->coins);
520 0 : GNUNET_free (proh->base_url);
521 0 : TALER_EXCHANGE_keys_decref (proh->keys);
522 0 : GNUNET_free (proh);
523 0 : return NULL;
524 : }
525 6 : return proh;
526 : }
527 :
528 :
529 : enum TALER_ErrorCode
530 6 : TALER_EXCHANGE_post_reserves_open_start (
531 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh,
532 : TALER_EXCHANGE_PostReservesOpenCallback cb,
533 : TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls)
534 : {
535 : CURL *eh;
536 : char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
537 :
538 6 : proh->cb = cb;
539 6 : proh->cb_cls = cb_cls;
540 : {
541 : char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2];
542 : char *end;
543 :
544 6 : end = GNUNET_STRINGS_data_to_string (
545 6 : &proh->reserve_pub,
546 : sizeof (proh->reserve_pub),
547 : pub_str,
548 : sizeof (pub_str));
549 6 : *end = '\0';
550 6 : GNUNET_snprintf (arg_str,
551 : sizeof (arg_str),
552 : "reserves/%s/open",
553 : pub_str);
554 : }
555 6 : proh->url = TALER_url_join (proh->base_url,
556 : arg_str,
557 : NULL);
558 6 : if (NULL == proh->url)
559 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
560 6 : eh = TALER_EXCHANGE_curl_easy_get_ (proh->url);
561 12 : if ( (NULL == eh) ||
562 : (GNUNET_OK !=
563 6 : TALER_curl_easy_post (&proh->post_ctx,
564 : eh,
565 6 : proh->body)) )
566 : {
567 0 : GNUNET_break (0);
568 0 : if (NULL != eh)
569 0 : curl_easy_cleanup (eh);
570 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
571 : }
572 12 : proh->job = GNUNET_CURL_job_add2 (proh->ctx,
573 : eh,
574 6 : proh->post_ctx.headers,
575 : &handle_reserves_open_finished,
576 : proh);
577 6 : if (NULL == proh->job)
578 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
579 6 : return TALER_EC_NONE;
580 : }
581 :
582 :
583 : void
584 6 : TALER_EXCHANGE_post_reserves_open_cancel (
585 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh)
586 : {
587 6 : if (NULL != proh->job)
588 : {
589 0 : GNUNET_CURL_job_cancel (proh->job);
590 0 : proh->job = NULL;
591 : }
592 6 : TALER_curl_easy_post_finished (&proh->post_ctx);
593 6 : json_decref (proh->body);
594 6 : GNUNET_free (proh->coins);
595 6 : GNUNET_free (proh->url);
596 6 : GNUNET_free (proh->base_url);
597 6 : TALER_EXCHANGE_keys_decref (proh->keys);
598 6 : GNUNET_free (proh);
599 6 : }
600 :
601 :
602 : /* end of exchange_api_post-reserves-RESERVE_PUB-open.c */
|