Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2022-2023 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_purse_create_with_merge.c
19 : * @brief Implementation of the client to create a
20 : * purse for an account
21 : * @author Christian Grothoff
22 : */
23 : #include "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_json_lib.h"
30 : #include "taler_exchange_service.h"
31 : #include "exchange_api_handle.h"
32 : #include "exchange_api_common.h"
33 : #include "taler_signatures.h"
34 : #include "exchange_api_curl_defaults.h"
35 :
36 :
37 : /**
38 : * @brief A purse create with merge handle
39 : */
40 : struct TALER_EXCHANGE_PurseCreateMergeHandle
41 : {
42 :
43 : /**
44 : * The keys of the exchange this request handle will use
45 : */
46 : struct TALER_EXCHANGE_Keys *keys;
47 :
48 : /**
49 : * The url for this request.
50 : */
51 : char *url;
52 :
53 : /**
54 : * The exchange base URL.
55 : */
56 : char *exchange_url;
57 :
58 : /**
59 : * Context for #TEH_curl_easy_post(). Keeps the data that must
60 : * persist for Curl to make the upload.
61 : */
62 : struct TALER_CURL_PostContext ctx;
63 :
64 : /**
65 : * Handle for the request.
66 : */
67 : struct GNUNET_CURL_Job *job;
68 :
69 : /**
70 : * Function to call with the result.
71 : */
72 : TALER_EXCHANGE_PurseCreateMergeCallback cb;
73 :
74 : /**
75 : * Closure for @a cb.
76 : */
77 : void *cb_cls;
78 :
79 : /**
80 : * The encrypted contract (if any).
81 : */
82 : struct TALER_EncryptedContract econtract;
83 :
84 : /**
85 : * Expected value in the purse after fees.
86 : */
87 : struct TALER_Amount purse_value_after_fees;
88 :
89 : /**
90 : * Public key of the reserve public key.
91 : */
92 : struct TALER_ReservePublicKeyP reserve_pub;
93 :
94 : /**
95 : * Reserve signature affirming our merge.
96 : */
97 : struct TALER_ReserveSignatureP reserve_sig;
98 :
99 : /**
100 : * Merge capability key.
101 : */
102 : struct TALER_PurseMergePublicKeyP merge_pub;
103 :
104 : /**
105 : * Our merge signature (if any).
106 : */
107 : struct TALER_PurseMergeSignatureP merge_sig;
108 :
109 : /**
110 : * Public key of the purse.
111 : */
112 : struct TALER_PurseContractPublicKeyP purse_pub;
113 :
114 : /**
115 : * Request data we signed over.
116 : */
117 : struct TALER_PurseContractSignatureP purse_sig;
118 :
119 : /**
120 : * Hash over the purse's contrac terms.
121 : */
122 : struct TALER_PrivateContractHashP h_contract_terms;
123 :
124 : /**
125 : * When does the purse expire.
126 : */
127 : struct GNUNET_TIME_Timestamp purse_expiration;
128 :
129 : /**
130 : * When does the purse get merged/created.
131 : */
132 : struct GNUNET_TIME_Timestamp merge_timestamp;
133 : };
134 :
135 :
136 : /**
137 : * Function called when we're done processing the
138 : * HTTP /reserves/$RID/purse request.
139 : *
140 : * @param cls the `struct TALER_EXCHANGE_PurseCreateMergeHandle`
141 : * @param response_code HTTP response code, 0 on error
142 : * @param response parsed JSON result, NULL on error
143 : */
144 : static void
145 14 : handle_purse_create_with_merge_finished (void *cls,
146 : long response_code,
147 : const void *response)
148 : {
149 14 : struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm = cls;
150 14 : const json_t *j = response;
151 14 : struct TALER_EXCHANGE_PurseCreateMergeResponse dr = {
152 : .hr.reply = j,
153 14 : .hr.http_status = (unsigned int) response_code,
154 14 : .reserve_sig = &pcm->reserve_sig
155 : };
156 :
157 14 : pcm->job = NULL;
158 14 : switch (response_code)
159 : {
160 0 : case 0:
161 0 : dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
162 0 : break;
163 11 : case MHD_HTTP_OK:
164 : {
165 : struct GNUNET_TIME_Timestamp etime;
166 : struct TALER_Amount total_deposited;
167 : struct TALER_ExchangeSignatureP exchange_sig;
168 : struct TALER_ExchangePublicKeyP exchange_pub;
169 : struct GNUNET_JSON_Specification spec[] = {
170 11 : TALER_JSON_spec_amount_any ("total_deposited",
171 : &total_deposited),
172 11 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
173 : &exchange_sig),
174 11 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
175 : &exchange_pub),
176 11 : GNUNET_JSON_spec_timestamp ("exchange_timestamp",
177 : &etime),
178 11 : GNUNET_JSON_spec_end ()
179 : };
180 :
181 11 : if (GNUNET_OK !=
182 11 : GNUNET_JSON_parse (j,
183 : spec,
184 : NULL, NULL))
185 : {
186 0 : GNUNET_break_op (0);
187 0 : dr.hr.http_status = 0;
188 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
189 0 : break;
190 : }
191 11 : if (GNUNET_OK !=
192 11 : TALER_EXCHANGE_test_signing_key (pcm->keys,
193 : &exchange_pub))
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 11 : if (GNUNET_OK !=
201 11 : TALER_exchange_online_purse_created_verify (
202 : etime,
203 : pcm->purse_expiration,
204 11 : &pcm->purse_value_after_fees,
205 : &total_deposited,
206 11 : &pcm->purse_pub,
207 11 : &pcm->h_contract_terms,
208 : &exchange_pub,
209 : &exchange_sig))
210 : {
211 0 : GNUNET_break_op (0);
212 0 : dr.hr.http_status = 0;
213 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
214 0 : break;
215 : }
216 : }
217 11 : break;
218 0 : case MHD_HTTP_BAD_REQUEST:
219 : /* This should never happen, either us or the exchange is buggy
220 : (or API version conflict); just pass JSON reply to the application */
221 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
222 0 : dr.hr.hint = TALER_JSON_get_error_hint (j);
223 0 : break;
224 0 : case MHD_HTTP_FORBIDDEN:
225 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
226 0 : dr.hr.hint = TALER_JSON_get_error_hint (j);
227 : /* Nothing really to verify, exchange says one of the signatures is
228 : invalid; as we checked them, this should never happen, we
229 : should pass the JSON reply to the application */
230 0 : break;
231 0 : case MHD_HTTP_NOT_FOUND:
232 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
233 0 : dr.hr.hint = TALER_JSON_get_error_hint (j);
234 : /* Nothing really to verify, this should never
235 : happen, we should pass the JSON reply to the application */
236 0 : break;
237 2 : case MHD_HTTP_CONFLICT:
238 2 : dr.hr.ec = TALER_JSON_get_error_code (j);
239 2 : switch (dr.hr.ec)
240 : {
241 0 : case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA:
242 0 : if (GNUNET_OK !=
243 0 : TALER_EXCHANGE_check_purse_create_conflict_ (
244 0 : &pcm->purse_sig,
245 0 : &pcm->purse_pub,
246 : j))
247 : {
248 0 : dr.hr.http_status = 0;
249 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
250 0 : break;
251 : }
252 0 : break;
253 0 : case TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA:
254 0 : if (GNUNET_OK !=
255 0 : TALER_EXCHANGE_check_purse_merge_conflict_ (
256 0 : &pcm->merge_sig,
257 0 : &pcm->merge_pub,
258 0 : &pcm->purse_pub,
259 0 : pcm->exchange_url,
260 : j))
261 : {
262 0 : GNUNET_break_op (0);
263 0 : dr.hr.http_status = 0;
264 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
265 0 : break;
266 : }
267 0 : break;
268 2 : case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS:
269 : /* nothing to verify */
270 2 : break;
271 0 : case TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA:
272 0 : if (GNUNET_OK !=
273 0 : TALER_EXCHANGE_check_purse_econtract_conflict_ (
274 0 : &pcm->econtract.econtract_sig,
275 0 : &pcm->purse_pub,
276 : j))
277 : {
278 0 : GNUNET_break_op (0);
279 0 : dr.hr.http_status = 0;
280 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
281 0 : break;
282 : }
283 0 : break;
284 0 : default:
285 : /* unexpected EC! */
286 0 : GNUNET_break_op (0);
287 0 : dr.hr.http_status = 0;
288 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
289 0 : break;
290 : } /* end inner (EC) switch */
291 2 : break;
292 0 : case MHD_HTTP_GONE:
293 : /* could happen if denomination was revoked */
294 : /* Note: one might want to check /keys for revocation
295 : signature here, alas tricky in case our /keys
296 : is outdated => left to clients */
297 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
298 0 : dr.hr.hint = TALER_JSON_get_error_hint (j);
299 0 : break;
300 1 : case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
301 : {
302 : struct GNUNET_JSON_Specification spec[] = {
303 1 : GNUNET_JSON_spec_uint64 (
304 : "requirement_row",
305 : &dr.details.unavailable_for_legal_reasons.requirement_row),
306 1 : GNUNET_JSON_spec_end ()
307 : };
308 :
309 1 : if (GNUNET_OK !=
310 1 : GNUNET_JSON_parse (j,
311 : spec,
312 : NULL, NULL))
313 : {
314 0 : GNUNET_break_op (0);
315 0 : dr.hr.http_status = 0;
316 0 : dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
317 0 : break;
318 : }
319 : }
320 1 : break;
321 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
322 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
323 0 : dr.hr.hint = TALER_JSON_get_error_hint (j);
324 : /* Server had an internal issue; we should retry, but this API
325 : leaves this to the application */
326 0 : break;
327 0 : default:
328 : /* unexpected response code */
329 0 : dr.hr.ec = TALER_JSON_get_error_code (j);
330 0 : dr.hr.hint = TALER_JSON_get_error_hint (j);
331 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
332 : "Unexpected response code %u/%d for exchange deposit\n",
333 : (unsigned int) response_code,
334 : dr.hr.ec);
335 0 : GNUNET_break_op (0);
336 0 : break;
337 : }
338 14 : pcm->cb (pcm->cb_cls,
339 : &dr);
340 14 : TALER_EXCHANGE_purse_create_with_merge_cancel (pcm);
341 14 : }
342 :
343 :
344 : struct TALER_EXCHANGE_PurseCreateMergeHandle *
345 14 : TALER_EXCHANGE_purse_create_with_merge (
346 : struct GNUNET_CURL_Context *ctx,
347 : const char *url,
348 : struct TALER_EXCHANGE_Keys *keys,
349 : const struct TALER_ReservePrivateKeyP *reserve_priv,
350 : const struct TALER_PurseContractPrivateKeyP *purse_priv,
351 : const struct TALER_PurseMergePrivateKeyP *merge_priv,
352 : const struct TALER_ContractDiffiePrivateP *contract_priv,
353 : const json_t *contract_terms,
354 : bool upload_contract,
355 : bool pay_for_purse,
356 : struct GNUNET_TIME_Timestamp merge_timestamp,
357 : TALER_EXCHANGE_PurseCreateMergeCallback cb,
358 : void *cb_cls)
359 : {
360 : struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm;
361 : json_t *create_with_merge_obj;
362 : CURL *eh;
363 : char arg_str[sizeof (pcm->reserve_pub) * 2 + 32];
364 14 : uint32_t min_age = 0;
365 : struct TALER_Amount purse_fee;
366 : enum TALER_WalletAccountMergeFlags flags;
367 :
368 14 : pcm = GNUNET_new (struct TALER_EXCHANGE_PurseCreateMergeHandle);
369 14 : pcm->cb = cb;
370 14 : pcm->cb_cls = cb_cls;
371 14 : if (GNUNET_OK !=
372 14 : TALER_JSON_contract_hash (contract_terms,
373 : &pcm->h_contract_terms))
374 : {
375 0 : GNUNET_break (0);
376 0 : GNUNET_free (pcm);
377 0 : return NULL;
378 : }
379 14 : pcm->merge_timestamp = merge_timestamp;
380 14 : GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv,
381 : &pcm->purse_pub.eddsa_pub);
382 14 : GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
383 : &pcm->reserve_pub.eddsa_pub);
384 14 : GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv,
385 : &pcm->merge_pub.eddsa_pub);
386 :
387 : {
388 : struct GNUNET_JSON_Specification spec[] = {
389 14 : TALER_JSON_spec_amount_any ("amount",
390 : &pcm->purse_value_after_fees),
391 14 : GNUNET_JSON_spec_mark_optional (
392 : GNUNET_JSON_spec_uint32 ("minimum_age",
393 : &min_age),
394 : NULL),
395 14 : GNUNET_JSON_spec_timestamp ("pay_deadline",
396 : &pcm->purse_expiration),
397 14 : GNUNET_JSON_spec_end ()
398 : };
399 :
400 14 : if (GNUNET_OK !=
401 14 : GNUNET_JSON_parse (contract_terms,
402 : spec,
403 : NULL, NULL))
404 : {
405 0 : GNUNET_break (0);
406 0 : GNUNET_free (pcm);
407 0 : return NULL;
408 : }
409 : }
410 14 : if (pay_for_purse)
411 : {
412 : const struct TALER_EXCHANGE_GlobalFee *gf;
413 :
414 8 : gf = TALER_EXCHANGE_get_global_fee (
415 : keys,
416 : GNUNET_TIME_timestamp_get ());
417 8 : purse_fee = gf->fees.purse;
418 8 : flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE;
419 : }
420 : else
421 : {
422 6 : GNUNET_assert (GNUNET_OK ==
423 : TALER_amount_set_zero (pcm->purse_value_after_fees.currency,
424 : &purse_fee));
425 6 : flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA;
426 : }
427 :
428 : {
429 : char pub_str[sizeof (pcm->reserve_pub) * 2];
430 : char *end;
431 :
432 14 : end = GNUNET_STRINGS_data_to_string (
433 14 : &pcm->reserve_pub,
434 : sizeof (pcm->reserve_pub),
435 : pub_str,
436 : sizeof (pub_str));
437 14 : *end = '\0';
438 14 : GNUNET_snprintf (arg_str,
439 : sizeof (arg_str),
440 : "reserves/%s/purse",
441 : pub_str);
442 : }
443 14 : pcm->url = TALER_url_join (url,
444 : arg_str,
445 : NULL);
446 14 : if (NULL == pcm->url)
447 : {
448 0 : GNUNET_break (0);
449 0 : GNUNET_free (pcm);
450 0 : return NULL;
451 : }
452 14 : TALER_wallet_purse_create_sign (pcm->purse_expiration,
453 14 : &pcm->h_contract_terms,
454 14 : &pcm->merge_pub,
455 : min_age,
456 14 : &pcm->purse_value_after_fees,
457 : purse_priv,
458 : &pcm->purse_sig);
459 : {
460 : struct TALER_NormalizedPayto payto_uri;
461 :
462 14 : payto_uri = TALER_reserve_make_payto (url,
463 14 : &pcm->reserve_pub);
464 14 : TALER_wallet_purse_merge_sign (payto_uri,
465 : merge_timestamp,
466 14 : &pcm->purse_pub,
467 : merge_priv,
468 : &pcm->merge_sig);
469 14 : GNUNET_free (payto_uri.normalized_payto);
470 : }
471 14 : TALER_wallet_account_merge_sign (merge_timestamp,
472 14 : &pcm->purse_pub,
473 : pcm->purse_expiration,
474 14 : &pcm->h_contract_terms,
475 14 : &pcm->purse_value_after_fees,
476 : &purse_fee,
477 : min_age,
478 : flags,
479 : reserve_priv,
480 : &pcm->reserve_sig);
481 14 : if (upload_contract)
482 : {
483 14 : TALER_CRYPTO_contract_encrypt_for_deposit (
484 14 : &pcm->purse_pub,
485 : contract_priv,
486 : contract_terms,
487 : &pcm->econtract.econtract,
488 : &pcm->econtract.econtract_size);
489 14 : GNUNET_CRYPTO_ecdhe_key_get_public (&contract_priv->ecdhe_priv,
490 : &pcm->econtract.contract_pub.ecdhe_pub);
491 14 : TALER_wallet_econtract_upload_sign (
492 14 : pcm->econtract.econtract,
493 : pcm->econtract.econtract_size,
494 14 : &pcm->econtract.contract_pub,
495 : purse_priv,
496 : &pcm->econtract.econtract_sig);
497 : }
498 14 : create_with_merge_obj = GNUNET_JSON_PACK (
499 : TALER_JSON_pack_amount ("purse_value",
500 : &pcm->purse_value_after_fees),
501 : GNUNET_JSON_pack_uint64 ("min_age",
502 : min_age),
503 : GNUNET_JSON_pack_allow_null (
504 : TALER_JSON_pack_econtract ("econtract",
505 : upload_contract
506 : ? &pcm->econtract
507 : : NULL)),
508 : GNUNET_JSON_pack_allow_null (
509 : pay_for_purse
510 : ? TALER_JSON_pack_amount ("purse_fee",
511 : &purse_fee)
512 : : GNUNET_JSON_pack_string ("dummy2",
513 : NULL)),
514 : GNUNET_JSON_pack_data_auto ("merge_pub",
515 : &pcm->merge_pub),
516 : GNUNET_JSON_pack_data_auto ("merge_sig",
517 : &pcm->merge_sig),
518 : GNUNET_JSON_pack_data_auto ("reserve_sig",
519 : &pcm->reserve_sig),
520 : GNUNET_JSON_pack_data_auto ("purse_pub",
521 : &pcm->purse_pub),
522 : GNUNET_JSON_pack_data_auto ("purse_sig",
523 : &pcm->purse_sig),
524 : GNUNET_JSON_pack_data_auto ("h_contract_terms",
525 : &pcm->h_contract_terms),
526 : GNUNET_JSON_pack_timestamp ("merge_timestamp",
527 : merge_timestamp),
528 : GNUNET_JSON_pack_timestamp ("purse_expiration",
529 : pcm->purse_expiration));
530 14 : GNUNET_assert (NULL != create_with_merge_obj);
531 14 : eh = TALER_EXCHANGE_curl_easy_get_ (pcm->url);
532 28 : if ( (NULL == eh) ||
533 : (GNUNET_OK !=
534 14 : TALER_curl_easy_post (&pcm->ctx,
535 : eh,
536 : create_with_merge_obj)) )
537 : {
538 0 : GNUNET_break (0);
539 0 : if (NULL != eh)
540 0 : curl_easy_cleanup (eh);
541 0 : json_decref (create_with_merge_obj);
542 0 : GNUNET_free (pcm->econtract.econtract);
543 0 : GNUNET_free (pcm->url);
544 0 : GNUNET_free (pcm);
545 0 : return NULL;
546 : }
547 14 : json_decref (create_with_merge_obj);
548 14 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
549 : "URL for purse create_with_merge: `%s'\n",
550 : pcm->url);
551 14 : pcm->keys = TALER_EXCHANGE_keys_incref (keys);
552 14 : pcm->exchange_url = GNUNET_strdup (url);
553 28 : pcm->job = GNUNET_CURL_job_add2 (ctx,
554 : eh,
555 14 : pcm->ctx.headers,
556 : &handle_purse_create_with_merge_finished,
557 : pcm);
558 14 : return pcm;
559 : }
560 :
561 :
562 : void
563 14 : TALER_EXCHANGE_purse_create_with_merge_cancel (
564 : struct TALER_EXCHANGE_PurseCreateMergeHandle *pcm)
565 : {
566 14 : if (NULL != pcm->job)
567 : {
568 0 : GNUNET_CURL_job_cancel (pcm->job);
569 0 : pcm->job = NULL;
570 : }
571 14 : GNUNET_free (pcm->url);
572 14 : GNUNET_free (pcm->exchange_url);
573 14 : TALER_curl_easy_post_finished (&pcm->ctx);
574 14 : TALER_EXCHANGE_keys_decref (pcm->keys);
575 14 : GNUNET_free (pcm->econtract.econtract);
576 14 : GNUNET_free (pcm);
577 14 : }
578 :
579 :
580 : /* end of exchange_api_purse_create_with_merge.c */
|