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 : * Function called when we're done processing the
138 : * HTTP /purses/$PID/deposit request.
139 : *
140 : * @param cls the `struct TALER_EXCHANGE_PostPursesDepositHandle`
141 : * @param response_code HTTP response code, 0 on error
142 : * @param response parsed JSON result, NULL on error
143 : */
144 : static void
145 7 : handle_purse_deposit_finished (void *cls,
146 : long response_code,
147 : const void *response)
148 : {
149 7 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch = cls;
150 7 : const json_t *j = response;
151 7 : struct TALER_EXCHANGE_PostPursesDepositResponse dr = {
152 : .hr.reply = j,
153 7 : .hr.http_status = (unsigned int) response_code
154 : };
155 7 : const struct TALER_EXCHANGE_Keys *keys = pch->keys;
156 :
157 7 : pch->job = NULL;
158 7 : switch (response_code)
159 : {
160 0 : case 0:
161 0 : dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
162 0 : break;
163 5 : case MHD_HTTP_OK:
164 : {
165 : struct GNUNET_TIME_Timestamp etime;
166 : struct GNUNET_JSON_Specification spec[] = {
167 5 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
168 : &dr.details.ok.exchange_sig),
169 5 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
170 : &dr.details.ok.exchange_pub),
171 5 : GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
172 : &dr.details.ok.h_contract_terms),
173 5 : GNUNET_JSON_spec_timestamp ("exchange_timestamp",
174 : &etime),
175 5 : GNUNET_JSON_spec_timestamp ("purse_expiration",
176 : &dr.details.ok.purse_expiration),
177 5 : TALER_JSON_spec_amount ("total_deposited",
178 5 : keys->currency,
179 : &dr.details.ok.total_deposited),
180 5 : TALER_JSON_spec_amount ("purse_value_after_fees",
181 5 : keys->currency,
182 : &dr.details.ok.purse_value_after_fees),
183 5 : GNUNET_JSON_spec_end ()
184 : };
185 :
186 5 : if (GNUNET_OK !=
187 5 : GNUNET_JSON_parse (j,
188 : spec,
189 : NULL, NULL))
190 : {
191 0 : GNUNET_break_op (0);
192 0 : dr.hr.http_status = 0;
193 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
194 0 : break;
195 : }
196 5 : if (GNUNET_OK !=
197 5 : TALER_EXCHANGE_test_signing_key (keys,
198 : &dr.details.ok.exchange_pub))
199 : {
200 0 : GNUNET_break_op (0);
201 0 : dr.hr.http_status = 0;
202 0 : dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
203 0 : break;
204 : }
205 5 : if (GNUNET_OK !=
206 5 : TALER_exchange_online_purse_created_verify (
207 : etime,
208 : dr.details.ok.purse_expiration,
209 : &dr.details.ok.purse_value_after_fees,
210 : &dr.details.ok.total_deposited,
211 5 : &pch->purse_pub,
212 : &dr.details.ok.h_contract_terms,
213 : &dr.details.ok.exchange_pub,
214 : &dr.details.ok.exchange_sig))
215 : {
216 0 : GNUNET_break_op (0);
217 0 : dr.hr.http_status = 0;
218 0 : dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
219 0 : break;
220 : }
221 : }
222 5 : break;
223 0 : case MHD_HTTP_BAD_REQUEST:
224 : /* This should never happen, either us or the exchange is buggy
225 : (or API version conflict); just pass JSON reply to the application */
226 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
227 0 : break;
228 0 : case MHD_HTTP_FORBIDDEN:
229 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
230 : /* Nothing really to verify, exchange says one of the signatures is
231 : invalid; as we checked them, this should never happen, we
232 : should pass the JSON reply to the application */
233 0 : break;
234 0 : case MHD_HTTP_NOT_FOUND:
235 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
236 : /* Nothing really to verify, this should never
237 : happen, we should pass the JSON reply to the application */
238 0 : break;
239 2 : case MHD_HTTP_CONFLICT:
240 2 : dr.hr.ec = TALER_JSON_get_error_code (j);
241 2 : switch (dr.hr.ec)
242 : {
243 0 : case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA:
244 : {
245 : struct TALER_CoinSpendPublicKeyP coin_pub;
246 : struct TALER_CoinSpendSignatureP coin_sig;
247 : struct TALER_DenominationHashP h_denom_pub;
248 : struct TALER_AgeCommitmentHashP phac;
249 0 : bool found = false;
250 :
251 0 : if (GNUNET_OK !=
252 0 : TALER_EXCHANGE_check_purse_coin_conflict_ (
253 0 : &pch->purse_pub,
254 0 : pch->base_url,
255 : j,
256 : &h_denom_pub,
257 : &phac,
258 : &coin_pub,
259 : &coin_sig))
260 : {
261 0 : GNUNET_break_op (0);
262 0 : dr.hr.http_status = 0;
263 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
264 0 : break;
265 : }
266 0 : for (unsigned int i = 0; i<pch->num_deposits; i++)
267 : {
268 0 : struct Coin *coin = &pch->coins[i];
269 0 : if (0 != GNUNET_memcmp (&coin_pub,
270 : &coin->coin_pub))
271 0 : continue;
272 0 : if (0 !=
273 0 : GNUNET_memcmp (&coin->h_denom_pub,
274 : &h_denom_pub))
275 : {
276 0 : found = true;
277 0 : break;
278 : }
279 0 : if (0 !=
280 0 : GNUNET_memcmp (&coin->ahac,
281 : &phac))
282 : {
283 0 : found = true;
284 0 : break;
285 : }
286 0 : if (0 == GNUNET_memcmp (&coin_sig,
287 : &coin->coin_sig))
288 : {
289 : /* identical signature => not a conflict */
290 0 : continue;
291 : }
292 0 : found = true;
293 0 : break;
294 : }
295 0 : if (! found)
296 : {
297 0 : GNUNET_break_op (0);
298 0 : dr.hr.http_status = 0;
299 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
300 0 : break;
301 : }
302 : /* meta data conflict is real! */
303 0 : break;
304 : }
305 2 : case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
306 : /* Nothing to check anymore here, proof needs to be
307 : checked in the GET /coins/$COIN_PUB handler */
308 2 : break;
309 0 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
310 0 : break;
311 0 : default:
312 0 : GNUNET_break_op (0);
313 0 : dr.hr.http_status = 0;
314 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
315 0 : break;
316 : } /* ec switch */
317 2 : break;
318 0 : case MHD_HTTP_GONE:
319 : /* could happen if denomination was revoked or purse expired */
320 : /* Note: one might want to check /keys for revocation
321 : signature here, alas tricky in case our /keys
322 : is outdated => left to clients */
323 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
324 0 : break;
325 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
326 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
327 : /* Server had an internal issue; we should retry, but this API
328 : leaves this to the application */
329 0 : break;
330 0 : default:
331 : /* unexpected response code */
332 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
333 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
334 : "Unexpected response code %u/%d for exchange deposit\n",
335 : (unsigned int) response_code,
336 : dr.hr.ec);
337 0 : GNUNET_break_op (0);
338 0 : break;
339 : }
340 7 : if (TALER_EC_NONE == dr.hr.ec)
341 5 : dr.hr.hint = NULL;
342 : else
343 2 : dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec);
344 7 : pch->cb (pch->cb_cls,
345 : &dr);
346 7 : TALER_EXCHANGE_post_purses_deposit_cancel (pch);
347 7 : }
348 :
349 :
350 : struct TALER_EXCHANGE_PostPursesDepositHandle *
351 7 : TALER_EXCHANGE_post_purses_deposit_create (
352 : struct GNUNET_CURL_Context *ctx,
353 : const char *url,
354 : struct TALER_EXCHANGE_Keys *keys,
355 : const char *purse_exchange_url,
356 : const struct TALER_PurseContractPublicKeyP *purse_pub,
357 : uint8_t min_age,
358 : unsigned int num_deposits,
359 : const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits])
360 7 : {
361 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch;
362 : json_t *deposit_arr;
363 :
364 : // FIXME: use purse_exchange_url for wad transfers (#7271)
365 : (void) purse_exchange_url;
366 7 : if (0 == num_deposits)
367 : {
368 0 : GNUNET_break (0);
369 0 : return NULL;
370 : }
371 7 : pch = GNUNET_new (struct TALER_EXCHANGE_PostPursesDepositHandle);
372 7 : pch->ctx = ctx;
373 7 : pch->base_url = GNUNET_strdup (url);
374 7 : pch->keys = TALER_EXCHANGE_keys_incref (keys);
375 7 : pch->purse_pub = *purse_pub;
376 7 : pch->num_deposits = num_deposits;
377 7 : pch->coins = GNUNET_new_array (num_deposits,
378 : struct Coin);
379 : // FIXME: move JSON construction into _start() function.
380 7 : deposit_arr = json_array ();
381 7 : GNUNET_assert (NULL != deposit_arr);
382 14 : for (unsigned int i = 0; i<num_deposits; i++)
383 : {
384 7 : const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i];
385 7 : const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof;
386 7 : struct Coin *coin = &pch->coins[i];
387 : json_t *jdeposit;
388 7 : struct TALER_AgeCommitmentHashP *achp = NULL;
389 : struct TALER_AgeAttestationP attest;
390 7 : struct TALER_AgeAttestationP *attestp = NULL;
391 :
392 7 : if (NULL != acp)
393 : {
394 0 : TALER_age_commitment_hash (&acp->commitment,
395 : &coin->ahac);
396 0 : achp = &coin->ahac;
397 0 : if (GNUNET_OK !=
398 0 : TALER_age_commitment_attest (acp,
399 : min_age,
400 : &attest))
401 : {
402 0 : GNUNET_break (0);
403 0 : json_decref (deposit_arr);
404 0 : GNUNET_free (pch->base_url);
405 0 : GNUNET_free (pch->coins);
406 0 : TALER_EXCHANGE_keys_decref (pch->keys);
407 0 : GNUNET_free (pch);
408 0 : return NULL;
409 : }
410 0 : attestp = &attest;
411 : }
412 7 : GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv,
413 : &coin->coin_pub.eddsa_pub);
414 7 : coin->h_denom_pub = deposit->h_denom_pub;
415 7 : coin->contribution = deposit->amount;
416 7 : TALER_wallet_purse_deposit_sign (
417 7 : pch->base_url,
418 7 : &pch->purse_pub,
419 : &deposit->amount,
420 7 : &coin->h_denom_pub,
421 7 : &coin->ahac,
422 : &deposit->coin_priv,
423 : &coin->coin_sig);
424 7 : jdeposit = GNUNET_JSON_PACK (
425 : GNUNET_JSON_pack_allow_null (
426 : GNUNET_JSON_pack_data_auto ("h_age_commitment",
427 : achp)),
428 : GNUNET_JSON_pack_allow_null (
429 : GNUNET_JSON_pack_data_auto ("age_attestation",
430 : attestp)),
431 : TALER_JSON_pack_amount ("amount",
432 : &deposit->amount),
433 : GNUNET_JSON_pack_data_auto ("denom_pub_hash",
434 : &deposit->h_denom_pub),
435 : TALER_JSON_pack_denom_sig ("ub_sig",
436 : &deposit->denom_sig),
437 : GNUNET_JSON_pack_data_auto ("coin_pub",
438 : &coin->coin_pub),
439 : GNUNET_JSON_pack_data_auto ("coin_sig",
440 : &coin->coin_sig));
441 7 : GNUNET_assert (0 ==
442 : json_array_append_new (deposit_arr,
443 : jdeposit));
444 : }
445 7 : pch->body = GNUNET_JSON_PACK (
446 : GNUNET_JSON_pack_array_steal ("deposits",
447 : deposit_arr));
448 7 : GNUNET_assert (NULL != pch->body);
449 7 : return pch;
450 : }
451 :
452 :
453 : enum TALER_ErrorCode
454 7 : TALER_EXCHANGE_post_purses_deposit_start (
455 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch,
456 : TALER_EXCHANGE_PostPursesDepositCallback cb,
457 : TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls)
458 : {
459 : CURL *eh;
460 : char arg_str[sizeof (pch->purse_pub) * 2 + 32];
461 :
462 7 : pch->cb = cb;
463 7 : pch->cb_cls = cb_cls;
464 : {
465 : char pub_str[sizeof (pch->purse_pub) * 2];
466 : char *end;
467 :
468 7 : end = GNUNET_STRINGS_data_to_string (
469 7 : &pch->purse_pub,
470 : sizeof (pch->purse_pub),
471 : pub_str,
472 : sizeof (pub_str));
473 7 : *end = '\0';
474 7 : GNUNET_snprintf (arg_str,
475 : sizeof (arg_str),
476 : "purses/%s/deposit",
477 : pub_str);
478 : }
479 7 : pch->url = TALER_url_join (pch->base_url,
480 : arg_str,
481 : NULL);
482 7 : if (NULL == pch->url)
483 : {
484 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
485 : "Could not construct request URL.\n");
486 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
487 : }
488 7 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
489 : "URL for purse deposit: `%s'\n",
490 : pch->url);
491 7 : eh = TALER_EXCHANGE_curl_easy_get_ (pch->url);
492 14 : if ( (NULL == eh) ||
493 : (GNUNET_OK !=
494 7 : TALER_curl_easy_post (&pch->post_ctx,
495 : eh,
496 7 : pch->body)) )
497 : {
498 0 : GNUNET_break (0);
499 0 : if (NULL != eh)
500 0 : curl_easy_cleanup (eh);
501 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
502 : }
503 14 : pch->job = GNUNET_CURL_job_add2 (pch->ctx,
504 : eh,
505 7 : pch->post_ctx.headers,
506 : &handle_purse_deposit_finished,
507 : pch);
508 7 : if (NULL == pch->job)
509 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
510 7 : return TALER_EC_NONE;
511 : }
512 :
513 :
514 : void
515 7 : TALER_EXCHANGE_post_purses_deposit_cancel (
516 : struct TALER_EXCHANGE_PostPursesDepositHandle *pch)
517 : {
518 7 : if (NULL != pch->job)
519 : {
520 0 : GNUNET_CURL_job_cancel (pch->job);
521 0 : pch->job = NULL;
522 : }
523 7 : TALER_curl_easy_post_finished (&pch->post_ctx);
524 7 : GNUNET_free (pch->base_url);
525 7 : GNUNET_free (pch->url);
526 7 : GNUNET_free (pch->coins);
527 7 : json_decref (pch->body);
528 7 : TALER_EXCHANGE_keys_decref (pch->keys);
529 7 : GNUNET_free (pch);
530 7 : }
531 :
532 :
533 : /* end of exchange_api_post-purses-PURSE_PUB-deposit.c */
|