Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2022-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-purses-PURSE_PUB-deposit.c
19 : * @brief Implementation of the client to create a purse with
20 : * an initial set of deposits (and a contract)
21 : * @author Christian Grothoff
22 : */
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/taler_json_lib.h"
29 : #include "exchange_api_common.h"
30 : #include "exchange_api_handle.h"
31 : #include "taler/taler_signatures.h"
32 : #include "exchange_api_curl_defaults.h"
33 :
34 :
35 : /**
36 : * Information we track per coin.
37 : */
38 : struct Coin
39 : {
40 : /**
41 : * Coin's public key.
42 : */
43 : struct TALER_CoinSpendPublicKeyP coin_pub;
44 :
45 : /**
46 : * Signature made with the coin.
47 : */
48 : struct TALER_CoinSpendSignatureP coin_sig;
49 :
50 : /**
51 : * Coin's denomination.
52 : */
53 : struct TALER_DenominationHashP h_denom_pub;
54 :
55 : /**
56 : * Age restriction hash for the coin.
57 : */
58 : struct TALER_AgeCommitmentHashP ahac;
59 :
60 : /**
61 : * How much did we say the coin contributed.
62 : */
63 : struct TALER_Amount contribution;
64 : };
65 :
66 :
67 : /**
68 : * @brief A purse deposit handle
69 : */
70 : struct TALER_EXCHANGE_PostPursesDepositHandle
71 : {
72 :
73 : /**
74 : * Reference to the execution context.
75 : */
76 : struct GNUNET_CURL_Context *ctx;
77 :
78 : /**
79 : * The base url of the exchange we are talking to.
80 : */
81 : char *base_url;
82 :
83 : /**
84 : * The full URL for this request, set during _start.
85 : */
86 : char *url;
87 :
88 : /**
89 : * Minor context that holds body and headers.
90 : */
91 : struct TALER_CURL_PostContext post_ctx;
92 :
93 : /**
94 : * Handle for the request.
95 : */
96 : struct GNUNET_CURL_Job *job;
97 :
98 : /**
99 : * Function to call with the result.
100 : */
101 : TALER_EXCHANGE_PostPursesDepositCallback cb;
102 :
103 : /**
104 : * Closure for @a cb.
105 : */
106 : TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls;
107 :
108 : /**
109 : * The keys of the exchange this request handle will use.
110 : */
111 : struct TALER_EXCHANGE_Keys *keys;
112 :
113 : /**
114 : * Public key of the purse.
115 : */
116 : struct TALER_PurseContractPublicKeyP purse_pub;
117 :
118 : /**
119 : * Array of @e num_deposits coins we are depositing.
120 : */
121 : struct Coin *coins;
122 :
123 : /**
124 : * Number of coins we are depositing.
125 : */
126 : unsigned int num_deposits;
127 :
128 : /**
129 : * Pre-built request body.
130 : */
131 : json_t *body;
132 :
133 : };
134 :
135 :
136 : /**
137 : * Find the deposited coin @a coin_pub and how we used it.
138 : *
139 : * @param cls a `struct TALER_EXCHANGE_PostPursesDepositHandle *`
140 : * @param coin_pub public key of the coin named in the conflict reply
141 : * @param[out] h_denom_pub set to the hash of the denomination we used
142 : * @param[out] h_age_commitment set to the age commitment hash we used, or NULL
143 : * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours
144 : */
145 : static enum GNUNET_GenericReturnValue
146 0 : purse_deposit_coin_lookup (void *cls,
147 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
148 : const struct TALER_DenominationHashP **h_denom_pub,
149 : const struct TALER_AgeCommitmentHashP **
150 : h_age_commitment)
151 : {
152 0 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch = cls;
153 :
154 0 : for (unsigned int i = 0; i<pch->num_deposits; i++)
155 : {
156 0 : const struct Coin *coin = &pch->coins[i];
157 :
158 0 : if (0 != GNUNET_memcmp (coin_pub,
159 : &coin->coin_pub))
160 0 : continue;
161 0 : *h_denom_pub = &coin->h_denom_pub;
162 0 : *h_age_commitment = GNUNET_is_zero (&coin->ahac)
163 : ? NULL
164 0 : : &coin->ahac;
165 0 : return GNUNET_OK;
166 : }
167 0 : return GNUNET_NO;
168 : }
169 :
170 :
171 : /**
172 : * Function called when we're done processing the
173 : * HTTP /purses/$PID/deposit request.
174 : *
175 : * @param cls the `struct TALER_EXCHANGE_PostPursesDepositHandle`
176 : * @param response_code HTTP response code, 0 on error
177 : * @param response parsed JSON result, NULL on error
178 : */
179 : static void
180 7 : handle_purse_deposit_finished (void *cls,
181 : long response_code,
182 : const void *response)
183 : {
184 7 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch = cls;
185 7 : const json_t *j = response;
186 7 : struct TALER_EXCHANGE_PostPursesDepositResponse dr = {
187 : .hr.reply = j,
188 7 : .hr.http_status = (unsigned int) response_code
189 : };
190 7 : const struct TALER_EXCHANGE_Keys *keys = pch->keys;
191 :
192 7 : pch->job = NULL;
193 7 : switch (response_code)
194 : {
195 0 : case 0:
196 0 : dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
197 0 : break;
198 5 : case MHD_HTTP_OK:
199 : {
200 : struct GNUNET_TIME_Timestamp etime;
201 : struct GNUNET_JSON_Specification spec[] = {
202 5 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
203 : &dr.details.ok.exchange_sig),
204 5 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
205 : &dr.details.ok.exchange_pub),
206 5 : GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
207 : &dr.details.ok.h_contract_terms),
208 5 : GNUNET_JSON_spec_timestamp ("exchange_timestamp",
209 : &etime),
210 5 : GNUNET_JSON_spec_timestamp ("purse_expiration",
211 : &dr.details.ok.purse_expiration),
212 5 : TALER_JSON_spec_amount ("total_deposited",
213 5 : keys->currency,
214 : &dr.details.ok.total_deposited),
215 5 : TALER_JSON_spec_amount ("purse_value_after_fees",
216 5 : keys->currency,
217 : &dr.details.ok.purse_value_after_fees),
218 5 : GNUNET_JSON_spec_end ()
219 : };
220 :
221 5 : if (GNUNET_OK !=
222 5 : GNUNET_JSON_parse (j,
223 : spec,
224 : NULL, NULL))
225 : {
226 0 : GNUNET_break_op (0);
227 0 : dr.hr.http_status = 0;
228 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
229 0 : break;
230 : }
231 5 : if (GNUNET_OK !=
232 5 : TALER_EXCHANGE_test_signing_key (keys,
233 : &dr.details.ok.exchange_pub))
234 : {
235 0 : GNUNET_break_op (0);
236 0 : dr.hr.http_status = 0;
237 0 : dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
238 0 : break;
239 : }
240 5 : if (GNUNET_OK !=
241 5 : TALER_exchange_online_purse_created_verify (
242 : etime,
243 : dr.details.ok.purse_expiration,
244 : &dr.details.ok.purse_value_after_fees,
245 : &dr.details.ok.total_deposited,
246 5 : &pch->purse_pub,
247 : &dr.details.ok.h_contract_terms,
248 : &dr.details.ok.exchange_pub,
249 : &dr.details.ok.exchange_sig))
250 : {
251 0 : GNUNET_break_op (0);
252 0 : dr.hr.http_status = 0;
253 0 : dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
254 0 : break;
255 : }
256 : }
257 5 : break;
258 0 : case MHD_HTTP_BAD_REQUEST:
259 : /* This should never happen, either us or the exchange is buggy
260 : (or API version conflict); just pass JSON reply to the application */
261 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
262 0 : break;
263 0 : case MHD_HTTP_FORBIDDEN:
264 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
265 : /* Nothing really to verify, exchange says one of the signatures is
266 : invalid; as we checked them, this should never happen, we
267 : should pass the JSON reply to the application */
268 0 : break;
269 0 : case MHD_HTTP_NOT_FOUND:
270 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
271 : /* Nothing really to verify, this should never
272 : happen, we should pass the JSON reply to the application */
273 0 : break;
274 2 : case MHD_HTTP_CONFLICT:
275 2 : dr.hr.ec = TALER_JSON_get_error_code (j);
276 2 : switch (dr.hr.ec)
277 : {
278 0 : case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA:
279 : {
280 : struct TALER_CoinSpendPublicKeyP coin_pub;
281 : struct TALER_CoinSpendSignatureP coin_sig;
282 : struct TALER_DenominationHashP h_denom_pub;
283 : struct TALER_AgeCommitmentHashP phac;
284 0 : bool found = false;
285 :
286 0 : if (GNUNET_OK !=
287 0 : TALER_EXCHANGE_check_purse_coin_conflict_ (
288 0 : &pch->purse_pub,
289 0 : pch->base_url,
290 : j,
291 : &h_denom_pub,
292 : &phac,
293 : &coin_pub,
294 : &coin_sig))
295 : {
296 0 : GNUNET_break_op (0);
297 0 : dr.hr.http_status = 0;
298 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
299 0 : break;
300 : }
301 0 : for (unsigned int i = 0; i<pch->num_deposits; i++)
302 : {
303 0 : struct Coin *coin = &pch->coins[i];
304 0 : if (0 != GNUNET_memcmp (&coin_pub,
305 : &coin->coin_pub))
306 0 : continue;
307 0 : if (0 !=
308 0 : GNUNET_memcmp (&coin->h_denom_pub,
309 : &h_denom_pub))
310 : {
311 0 : found = true;
312 0 : break;
313 : }
314 0 : if (0 !=
315 0 : GNUNET_memcmp (&coin->ahac,
316 : &phac))
317 : {
318 0 : found = true;
319 0 : break;
320 : }
321 0 : if (0 == GNUNET_memcmp (&coin_sig,
322 : &coin->coin_sig))
323 : {
324 : /* identical signature => not a conflict */
325 0 : continue;
326 : }
327 0 : found = true;
328 0 : break;
329 : }
330 0 : if (! found)
331 : {
332 0 : GNUNET_break_op (0);
333 0 : dr.hr.http_status = 0;
334 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
335 0 : break;
336 : }
337 : /* meta data conflict is real! */
338 0 : break;
339 : }
340 2 : case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
341 : /* Nothing to check anymore here, proof needs to be
342 : checked in the GET /coins/$COIN_PUB handler */
343 2 : break;
344 0 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
345 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH:
346 0 : if (GNUNET_OK !=
347 0 : TALER_EXCHANGE_check_coin_conflict_ (pch->keys,
348 : dr.hr.ec,
349 : j,
350 : &purse_deposit_coin_lookup,
351 : pch,
352 : &dr.details.conflict))
353 : {
354 0 : GNUNET_break_op (0);
355 0 : dr.hr.http_status = 0;
356 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
357 : }
358 0 : break;
359 0 : default:
360 0 : GNUNET_break_op (0);
361 0 : dr.hr.http_status = 0;
362 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
363 0 : break;
364 : } /* ec switch */
365 2 : break;
366 0 : case MHD_HTTP_GONE:
367 : /* could happen if denomination was revoked or purse expired */
368 : /* Note: one might want to check /keys for revocation
369 : signature here, alas tricky in case our /keys
370 : is outdated => left to clients */
371 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
372 0 : break;
373 0 : case MHD_HTTP_PRECONDITION_FAILED:
374 : /* could happen if we were too early and the denomination
375 : is not yet valid */
376 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
377 0 : break;
378 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
379 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
380 : /* Server had an internal issue; we should retry, but this API
381 : leaves this to the application */
382 0 : break;
383 0 : default:
384 : /* unexpected response code */
385 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
386 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
387 : "Unexpected response code %u/%d for exchange deposit\n",
388 : (unsigned int) response_code,
389 : dr.hr.ec);
390 0 : GNUNET_break_op (0);
391 0 : break;
392 : }
393 7 : if (TALER_EC_NONE == dr.hr.ec)
394 5 : dr.hr.hint = NULL;
395 : else
396 2 : dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec);
397 7 : pch->cb (pch->cb_cls,
398 : &dr);
399 7 : TALER_EXCHANGE_free_coin_conflict_ (&dr.hr,
400 : &dr.details.conflict);
401 7 : TALER_EXCHANGE_post_purses_deposit_cancel (pch);
402 7 : }
403 :
404 :
405 : struct TALER_EXCHANGE_PostPursesDepositHandle *
406 7 : TALER_EXCHANGE_post_purses_deposit_create (
407 : struct GNUNET_CURL_Context *ctx,
408 : const char *url,
409 : struct TALER_EXCHANGE_Keys *keys,
410 : const char *purse_exchange_url, // FIXME: turn into option!
411 : const struct TALER_PurseContractPublicKeyP *purse_pub,
412 : uint8_t min_age, // FIXME: turn into option!
413 : unsigned int num_deposits,
414 : const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits])
415 7 : {
416 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch;
417 : json_t *deposit_arr;
418 :
419 : // FIXME: use purse_exchange_url for wad transfers (#7271)
420 : (void) purse_exchange_url;
421 7 : if (0 == num_deposits)
422 : {
423 0 : GNUNET_break (0);
424 0 : return NULL;
425 : }
426 7 : pch = GNUNET_new (struct TALER_EXCHANGE_PostPursesDepositHandle);
427 7 : pch->ctx = ctx;
428 7 : pch->base_url = GNUNET_strdup (url);
429 7 : pch->keys = TALER_EXCHANGE_keys_incref (keys);
430 7 : pch->purse_pub = *purse_pub;
431 7 : pch->num_deposits = num_deposits;
432 7 : pch->coins = GNUNET_new_array (num_deposits,
433 : struct Coin);
434 : // FIXME: move JSON construction into _start() function.
435 7 : deposit_arr = json_array ();
436 7 : GNUNET_assert (NULL != deposit_arr);
437 14 : for (unsigned int i = 0; i<num_deposits; i++)
438 : {
439 7 : const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i];
440 7 : const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof;
441 7 : struct Coin *coin = &pch->coins[i];
442 : json_t *jdeposit;
443 7 : struct TALER_AgeCommitmentHashP *achp = NULL;
444 : struct TALER_AgeAttestationP attest;
445 7 : struct TALER_AgeAttestationP *attestp = NULL;
446 :
447 7 : if (NULL != acp)
448 : {
449 0 : TALER_age_commitment_hash (&acp->commitment,
450 : &coin->ahac);
451 0 : achp = &coin->ahac;
452 0 : if (GNUNET_OK !=
453 0 : TALER_age_commitment_attest (acp,
454 : min_age,
455 : &attest))
456 : {
457 0 : GNUNET_break (0);
458 0 : json_decref (deposit_arr);
459 0 : GNUNET_free (pch->base_url);
460 0 : GNUNET_free (pch->coins);
461 0 : TALER_EXCHANGE_keys_decref (pch->keys);
462 0 : GNUNET_free (pch);
463 0 : return NULL;
464 : }
465 0 : attestp = &attest;
466 : }
467 7 : GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv,
468 : &coin->coin_pub.eddsa_pub);
469 7 : coin->h_denom_pub = deposit->h_denom_pub;
470 7 : coin->contribution = deposit->amount;
471 7 : TALER_wallet_purse_deposit_sign (
472 7 : pch->base_url,
473 7 : &pch->purse_pub,
474 : &deposit->amount,
475 7 : &coin->h_denom_pub,
476 7 : &coin->ahac,
477 : &deposit->coin_priv,
478 : &coin->coin_sig);
479 7 : jdeposit = GNUNET_JSON_PACK (
480 : GNUNET_JSON_pack_allow_null (
481 : GNUNET_JSON_pack_data_auto ("h_age_commitment",
482 : achp)),
483 : GNUNET_JSON_pack_allow_null (
484 : GNUNET_JSON_pack_data_auto ("age_attestation",
485 : attestp)),
486 : TALER_JSON_pack_amount ("amount",
487 : &deposit->amount),
488 : GNUNET_JSON_pack_data_auto ("denom_pub_hash",
489 : &deposit->h_denom_pub),
490 : TALER_JSON_pack_denom_sig ("ub_sig",
491 : &deposit->denom_sig),
492 : GNUNET_JSON_pack_data_auto ("coin_pub",
493 : &coin->coin_pub),
494 : GNUNET_JSON_pack_data_auto ("coin_sig",
495 : &coin->coin_sig));
496 7 : GNUNET_assert (0 ==
497 : json_array_append_new (deposit_arr,
498 : jdeposit));
499 : }
500 7 : pch->body = GNUNET_JSON_PACK (
501 : GNUNET_JSON_pack_array_steal ("deposits",
502 : deposit_arr));
503 7 : GNUNET_assert (NULL != pch->body);
504 7 : return pch;
505 : }
506 :
507 :
508 : enum TALER_ErrorCode
509 7 : TALER_EXCHANGE_post_purses_deposit_start (
510 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch,
511 : TALER_EXCHANGE_PostPursesDepositCallback cb,
512 : TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls)
513 : {
514 : CURL *eh;
515 : char arg_str[sizeof (pch->purse_pub) * 2 + 32];
516 :
517 7 : pch->cb = cb;
518 7 : pch->cb_cls = cb_cls;
519 : {
520 : char pub_str[sizeof (pch->purse_pub) * 2];
521 : char *end;
522 :
523 7 : end = GNUNET_STRINGS_data_to_string (
524 7 : &pch->purse_pub,
525 : sizeof (pch->purse_pub),
526 : pub_str,
527 : sizeof (pub_str));
528 7 : *end = '\0';
529 7 : GNUNET_snprintf (arg_str,
530 : sizeof (arg_str),
531 : "purses/%s/deposit",
532 : pub_str);
533 : }
534 7 : pch->url = TALER_url_join (pch->base_url,
535 : arg_str,
536 : NULL);
537 7 : if (NULL == pch->url)
538 : {
539 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
540 : "Could not construct request URL.\n");
541 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
542 : }
543 7 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
544 : "URL for purse deposit: `%s'\n",
545 : pch->url);
546 7 : eh = TALER_EXCHANGE_curl_easy_get_ (pch->url);
547 14 : if ( (NULL == eh) ||
548 : (GNUNET_OK !=
549 7 : TALER_curl_easy_post (&pch->post_ctx,
550 : eh,
551 7 : pch->body)) )
552 : {
553 0 : GNUNET_break (0);
554 0 : if (NULL != eh)
555 0 : curl_easy_cleanup (eh);
556 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
557 : }
558 14 : pch->job = GNUNET_CURL_job_add2 (pch->ctx,
559 : eh,
560 7 : pch->post_ctx.headers,
561 : &handle_purse_deposit_finished,
562 : pch);
563 7 : if (NULL == pch->job)
564 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
565 7 : return TALER_EC_NONE;
566 : }
567 :
568 :
569 : void
570 7 : TALER_EXCHANGE_post_purses_deposit_cancel (
571 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch)
572 : {
573 7 : if (NULL != pch->job)
574 : {
575 0 : GNUNET_CURL_job_cancel (pch->job);
576 0 : pch->job = NULL;
577 : }
578 7 : TALER_curl_easy_post_finished (&pch->post_ctx);
579 7 : GNUNET_free (pch->base_url);
580 7 : GNUNET_free (pch->url);
581 7 : GNUNET_free (pch->coins);
582 7 : json_decref (pch->body);
583 7 : TALER_EXCHANGE_keys_decref (pch->keys);
584 7 : GNUNET_free (pch);
585 7 : }
586 :
587 :
588 : /* end of exchange_api_post-purses-PURSE_PUB-deposit.c */
|