Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2017-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 Affero 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 Affero General Public License for more details.
12 :
13 : You should have received a copy of the GNU Affero General Public License along with
14 : TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
15 : */
16 : /**
17 : * @file lib/exchange_api_post-recoup-refresh.c
18 : * @brief Implementation of the /recoup-refresh request of the exchange's HTTP API
19 : * @author Christian Grothoff
20 : * @author Özgür Kesim
21 : */
22 : #include <jansson.h>
23 : #include <microhttpd.h> /* just for HTTP status 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 : * @brief A /recoup-refresh handle
36 : */
37 : struct TALER_EXCHANGE_PostRecoupRefreshHandle
38 : {
39 :
40 : /**
41 : * The base URL for this request.
42 : */
43 : char *base_url;
44 :
45 : /**
46 : * The full URL for this request, set during _start.
47 : */
48 : char *url;
49 :
50 : /**
51 : * Minor context that holds body and headers.
52 : */
53 : struct TALER_CURL_PostContext post_ctx;
54 :
55 : /**
56 : * Handle for the request.
57 : */
58 : struct GNUNET_CURL_Job *job;
59 :
60 : /**
61 : * Function to call with the result.
62 : */
63 : TALER_EXCHANGE_PostRecoupRefreshCallback cb;
64 :
65 : /**
66 : * Closure for @a cb.
67 : */
68 : TALER_EXCHANGE_POST_RECOUP_REFRESH_RESULT_CLOSURE *cb_cls;
69 :
70 : /**
71 : * Reference to the execution context.
72 : */
73 : struct GNUNET_CURL_Context *ctx;
74 :
75 : /**
76 : * The keys of the exchange this request handle will use.
77 : */
78 : struct TALER_EXCHANGE_Keys *keys;
79 :
80 : /**
81 : * Old coin the coins were refreshed from.
82 : */
83 : struct TALER_CoinSpendPublicKeyP old_coin_pub;
84 :
85 : /**
86 : * Commitment of the refresh operation.
87 : */
88 : struct TALER_RefreshCommitmentP rc;
89 :
90 : /**
91 : * Public keys of the recouped coins, in request order.
92 : */
93 : struct TALER_CoinSpendPublicKeyP *recouped_pubs;
94 :
95 : /**
96 : * Denomination and age commitment of the recouped coins,
97 : * parallel to @e recouped_pubs.
98 : */
99 : struct TALER_EXCHANGE_RecoupedCoinInfo_ *recouped_infos;
100 :
101 : /**
102 : * Number of entries in @e recouped_pubs.
103 : */
104 : size_t num_recouped;
105 :
106 : /**
107 : * Pre-built request body.
108 : */
109 : json_t *body;
110 :
111 : };
112 :
113 :
114 : /**
115 : * Parse and verify a successful response. If it is valid,
116 : * call the callback.
117 : *
118 : * @param prrh request handle
119 : * @param json json reply with the signature
120 : * @return #GNUNET_OK if the response is valid and we called the callback;
121 : * #GNUNET_SYSERR if not (callback must still be called)
122 : */
123 : static enum GNUNET_GenericReturnValue
124 18 : process_ok_response (
125 : const struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh,
126 : const json_t *json)
127 : {
128 18 : struct TALER_EXCHANGE_PostRecoupRefreshResponse rr = {
129 : .hr.reply = json,
130 : .hr.http_status = MHD_HTTP_OK
131 : };
132 : const json_t *j_recoups;
133 : struct TALER_RecoupedCoin *recoups;
134 : struct GNUNET_HashCode h_recoups;
135 : struct GNUNET_JSON_Specification spec[] = {
136 18 : GNUNET_JSON_spec_fixed_auto ("old_coin_pub",
137 : &rr.details.ok.old_coin_pub),
138 18 : GNUNET_JSON_spec_fixed_auto ("rc",
139 : &rr.details.ok.rc),
140 18 : GNUNET_JSON_spec_timestamp ("timestamp",
141 : &rr.details.ok.timestamp),
142 18 : TALER_JSON_spec_amount_any ("total_amount",
143 : &rr.details.ok.total_amount),
144 18 : GNUNET_JSON_spec_array_const ("recoups",
145 : &j_recoups),
146 18 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
147 : &rr.details.ok.exchange_sig),
148 18 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
149 : &rr.details.ok.exchange_pub),
150 18 : GNUNET_JSON_spec_end ()
151 : };
152 :
153 18 : if (GNUNET_OK !=
154 18 : GNUNET_JSON_parse (json,
155 : spec,
156 : NULL, NULL))
157 : {
158 0 : GNUNET_break_op (0);
159 0 : return GNUNET_SYSERR;
160 : }
161 18 : if ( (0 !=
162 18 : GNUNET_memcmp (&rr.details.ok.old_coin_pub,
163 18 : &prrh->old_coin_pub)) ||
164 : (0 !=
165 18 : GNUNET_memcmp (&rr.details.ok.rc,
166 : &prrh->rc)) )
167 : {
168 0 : GNUNET_break_op (0);
169 0 : return GNUNET_SYSERR;
170 : }
171 18 : if (GNUNET_OK !=
172 18 : TALER_EXCHANGE_test_signing_key (prrh->keys,
173 : &rr.details.ok.exchange_pub))
174 : {
175 0 : GNUNET_break_op (0);
176 0 : return GNUNET_SYSERR;
177 : }
178 18 : if (GNUNET_OK !=
179 18 : TALER_EXCHANGE_parse_recoups_ (j_recoups,
180 18 : prrh->num_recouped,
181 18 : prrh->recouped_pubs,
182 : &rr.details.ok.total_amount,
183 : &recoups,
184 : &h_recoups))
185 : {
186 0 : GNUNET_break_op (0);
187 0 : return GNUNET_SYSERR;
188 : }
189 18 : if (GNUNET_OK !=
190 18 : TALER_exchange_online_confirm_recoup_refresh_batch_verify (
191 : rr.details.ok.timestamp,
192 : &rr.details.ok.old_coin_pub,
193 : &rr.details.ok.rc,
194 : &rr.details.ok.total_amount,
195 : &h_recoups,
196 : &rr.details.ok.exchange_pub,
197 : &rr.details.ok.exchange_sig))
198 : {
199 0 : GNUNET_break_op (0);
200 0 : GNUNET_free (recoups);
201 0 : return GNUNET_SYSERR;
202 : }
203 18 : rr.details.ok.num_recoups = prrh->num_recouped;
204 18 : rr.details.ok.recoups = recoups;
205 18 : prrh->cb (prrh->cb_cls,
206 : &rr);
207 18 : GNUNET_free (recoups);
208 18 : return GNUNET_OK;
209 : }
210 :
211 :
212 : /**
213 : * Find the recouped coin @a coin_pub and how we used it.
214 : *
215 : * @param cls a `struct TALER_EXCHANGE_PostRecoupRefreshHandle *`
216 : * @param coin_pub public key of the coin named in the conflict reply
217 : * @param[out] h_denom_pub set to the hash of the denomination we used
218 : * @param[out] h_age_commitment set to the age commitment hash we used, or NULL
219 : * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours
220 : */
221 : static enum GNUNET_GenericReturnValue
222 0 : recoup_coin_lookup (void *cls,
223 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
224 : const struct TALER_DenominationHashP **h_denom_pub,
225 : const struct TALER_AgeCommitmentHashP **h_age_commitment)
226 : {
227 0 : struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh = cls;
228 :
229 0 : for (size_t i = 0; i < prrh->num_recouped; i++)
230 : {
231 0 : if (0 != GNUNET_memcmp (coin_pub,
232 : &prrh->recouped_pubs[i]))
233 0 : continue;
234 0 : *h_denom_pub = &prrh->recouped_infos[i].h_denom_pub;
235 0 : *h_age_commitment = prrh->recouped_infos[i].have_age
236 0 : ? &prrh->recouped_infos[i].h_age_commitment
237 0 : : NULL;
238 0 : return GNUNET_OK;
239 : }
240 0 : return GNUNET_NO;
241 : }
242 :
243 :
244 : /**
245 : * Function called when we're done processing the
246 : * HTTP /recoup-refresh request.
247 : *
248 : * @param cls the `struct TALER_EXCHANGE_PostRecoupRefreshHandle`
249 : * @param response_code HTTP response code, 0 on error
250 : * @param response parsed JSON result, NULL on error
251 : */
252 : static void
253 20 : handle_recoup_refresh_finished (void *cls,
254 : long response_code,
255 : const void *response)
256 : {
257 20 : struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh = cls;
258 20 : const json_t *j = response;
259 20 : struct TALER_EXCHANGE_PostRecoupRefreshResponse rr = {
260 : .hr.reply = j,
261 20 : .hr.http_status = (unsigned int) response_code
262 : };
263 :
264 20 : prrh->job = NULL;
265 20 : switch (response_code)
266 : {
267 0 : case 0:
268 0 : rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
269 0 : break;
270 18 : case MHD_HTTP_OK:
271 18 : if (GNUNET_OK !=
272 18 : process_ok_response (prrh,
273 : j))
274 : {
275 0 : GNUNET_break_op (0);
276 0 : rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
277 0 : rr.hr.http_status = 0;
278 0 : break;
279 : }
280 18 : TALER_EXCHANGE_post_recoup_refresh_cancel (prrh);
281 18 : return;
282 0 : case MHD_HTTP_BAD_REQUEST:
283 : /* This should never happen, either us or the exchange is buggy
284 : (or API version conflict); just pass JSON reply to the application */
285 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
286 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
287 0 : break;
288 0 : case MHD_HTTP_FORBIDDEN:
289 : /* Nothing really to verify, exchange says one of the signatures is
290 : invalid; as we checked them, this should never happen, we
291 : should pass the JSON reply to the application */
292 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
293 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
294 0 : break;
295 0 : case MHD_HTTP_NOT_FOUND:
296 : /* Exchange does not know the denomination or the refresh
297 : operation; pass the JSON reply to the application */
298 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
299 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
300 0 : break;
301 0 : case MHD_HTTP_CONFLICT:
302 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
303 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
304 0 : switch (rr.hr.ec)
305 : {
306 0 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
307 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH:
308 0 : if (GNUNET_OK !=
309 0 : TALER_EXCHANGE_check_coin_conflict_ (prrh->keys,
310 : rr.hr.ec,
311 : j,
312 : &recoup_coin_lookup,
313 : prrh,
314 : &rr.details.conflict))
315 : {
316 0 : GNUNET_break_op (0);
317 0 : rr.hr.http_status = 0;
318 0 : rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
319 : }
320 0 : break;
321 0 : default:
322 : /* Commitment mismatch, denomination mismatch or a coin without
323 : residual value; pass the JSON reply to the application */
324 0 : break;
325 : }
326 0 : break;
327 2 : case MHD_HTTP_GONE:
328 : /* Denomination not eligible for recoup (not revoked, or expired) */
329 2 : rr.hr.ec = TALER_JSON_get_error_code (j);
330 2 : rr.hr.hint = TALER_JSON_get_error_hint (j);
331 2 : break;
332 0 : case MHD_HTTP_PRECONDITION_FAILED:
333 : /* Denomination not yet valid */
334 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
335 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
336 0 : break;
337 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
338 : /* Server had an internal issue; we should retry, but this API
339 : leaves this to the application */
340 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
341 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
342 0 : break;
343 0 : case MHD_HTTP_BAD_GATEWAY:
344 : case MHD_HTTP_SERVICE_UNAVAILABLE:
345 : /* Exchange is (temporarily) unable to sign; retry later */
346 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
347 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
348 0 : break;
349 0 : default:
350 : /* unexpected response code */
351 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
352 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
353 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
354 : "Unexpected response code %u/%d for exchange recoup-refresh\n",
355 : (unsigned int) response_code,
356 : (int) rr.hr.ec);
357 0 : GNUNET_break (0);
358 0 : break;
359 : }
360 2 : prrh->cb (prrh->cb_cls,
361 : &rr);
362 2 : TALER_EXCHANGE_free_coin_conflict_ (&rr.hr,
363 : &rr.details.conflict);
364 2 : TALER_EXCHANGE_post_recoup_refresh_cancel (prrh);
365 : }
366 :
367 :
368 : struct TALER_EXCHANGE_PostRecoupRefreshHandle *
369 20 : TALER_EXCHANGE_post_recoup_refresh_create (
370 : struct GNUNET_CURL_Context *ctx,
371 : const char *url,
372 : struct TALER_EXCHANGE_Keys *keys,
373 : const struct TALER_CoinSpendPublicKeyP *old_coin_pub,
374 : const struct TALER_RefreshCommitmentP *rc,
375 : const struct TALER_BlindingMasterSeedP *blinding_seed,
376 : size_t num_coins,
377 : const struct TALER_EXCHANGE_RecoupCoin coins[static num_coins])
378 20 : {
379 : struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh;
380 : json_t *coin_data;
381 :
382 20 : prrh = GNUNET_new (struct TALER_EXCHANGE_PostRecoupRefreshHandle);
383 20 : prrh->recouped_pubs = GNUNET_new_array (num_coins,
384 : struct TALER_CoinSpendPublicKeyP);
385 20 : prrh->recouped_infos = GNUNET_new_array (
386 : num_coins,
387 : struct TALER_EXCHANGE_RecoupedCoinInfo_);
388 20 : coin_data = TALER_EXCHANGE_recoup_coin_data_ (num_coins,
389 : coins,
390 : blinding_seed,
391 : true, /* for melt */
392 : prrh->recouped_pubs,
393 : prrh->recouped_infos,
394 : &prrh->num_recouped);
395 20 : if (NULL == coin_data)
396 : {
397 0 : GNUNET_break (0);
398 0 : GNUNET_free (prrh->recouped_pubs);
399 0 : GNUNET_free (prrh->recouped_infos);
400 0 : GNUNET_free (prrh);
401 0 : return NULL;
402 : }
403 20 : prrh->ctx = ctx;
404 20 : prrh->base_url = GNUNET_strdup (url);
405 20 : prrh->keys = TALER_EXCHANGE_keys_incref (keys);
406 20 : prrh->old_coin_pub = *old_coin_pub;
407 20 : prrh->rc = *rc;
408 20 : prrh->body = GNUNET_JSON_PACK (
409 : GNUNET_JSON_pack_data_auto ("old_coin_pub",
410 : old_coin_pub),
411 : GNUNET_JSON_pack_data_auto ("rc",
412 : rc),
413 : GNUNET_JSON_pack_array_steal ("coin_data",
414 : coin_data));
415 20 : return prrh;
416 : }
417 :
418 :
419 : enum TALER_ErrorCode
420 20 : TALER_EXCHANGE_post_recoup_refresh_start (
421 : struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh,
422 : TALER_EXCHANGE_PostRecoupRefreshCallback cb,
423 : TALER_EXCHANGE_POST_RECOUP_REFRESH_RESULT_CLOSURE *cb_cls)
424 : {
425 : CURL *eh;
426 :
427 20 : prrh->cb = cb;
428 20 : prrh->cb_cls = cb_cls;
429 20 : prrh->url = TALER_url_join (prrh->base_url,
430 : "recoup-refresh",
431 : NULL);
432 20 : if (NULL == prrh->url)
433 : {
434 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
435 : "Could not construct request URL.\n");
436 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
437 : }
438 20 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
439 : "URL for recoup-refresh: `%s'\n",
440 : prrh->url);
441 20 : eh = TALER_EXCHANGE_curl_easy_get_ (prrh->url);
442 40 : if ( (NULL == eh) ||
443 : (GNUNET_OK !=
444 20 : TALER_curl_easy_post (&prrh->post_ctx,
445 : eh,
446 20 : prrh->body)) )
447 : {
448 0 : GNUNET_break (0);
449 0 : if (NULL != eh)
450 0 : curl_easy_cleanup (eh);
451 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
452 : }
453 40 : prrh->job = GNUNET_CURL_job_add2 (prrh->ctx,
454 : eh,
455 20 : prrh->post_ctx.headers,
456 : &handle_recoup_refresh_finished,
457 : prrh);
458 20 : if (NULL == prrh->job)
459 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
460 20 : return TALER_EC_NONE;
461 : }
462 :
463 :
464 : void
465 20 : TALER_EXCHANGE_post_recoup_refresh_cancel (
466 : struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh)
467 : {
468 20 : if (NULL != prrh->job)
469 : {
470 0 : GNUNET_CURL_job_cancel (prrh->job);
471 0 : prrh->job = NULL;
472 : }
473 20 : TALER_curl_easy_post_finished (&prrh->post_ctx);
474 20 : GNUNET_free (prrh->url);
475 20 : GNUNET_free (prrh->base_url);
476 20 : GNUNET_free (prrh->recouped_pubs);
477 20 : GNUNET_free (prrh->recouped_infos);
478 20 : json_decref (prrh->body);
479 20 : TALER_EXCHANGE_keys_decref (prrh->keys);
480 20 : GNUNET_free (prrh);
481 20 : }
482 :
483 :
484 : /* end of exchange_api_post-recoup-refresh.c */
|