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