Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2014-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_refund.c
19 : * @brief Implementation of the /refund request of the exchange's HTTP API
20 : * @author Christian Grothoff
21 : */
22 : #include "platform.h"
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_json_lib.h"
29 : #include "taler_exchange_service.h"
30 : #include "exchange_api_handle.h"
31 : #include "taler_signatures.h"
32 : #include "exchange_api_curl_defaults.h"
33 :
34 :
35 : /**
36 : * @brief A Refund Handle
37 : */
38 : struct TALER_EXCHANGE_RefundHandle
39 : {
40 :
41 : /**
42 : * The keys of the exchange this request handle will use
43 : */
44 : struct TALER_EXCHANGE_Keys *keys;
45 :
46 : /**
47 : * The url for this request.
48 : */
49 : char *url;
50 :
51 : /**
52 : * Context for #TEH_curl_easy_post(). Keeps the data that must
53 : * persist for Curl to make the upload.
54 : */
55 : struct TALER_CURL_PostContext ctx;
56 :
57 : /**
58 : * Handle for the request.
59 : */
60 : struct GNUNET_CURL_Job *job;
61 :
62 : /**
63 : * Function to call with the result.
64 : */
65 : TALER_EXCHANGE_RefundCallback cb;
66 :
67 : /**
68 : * Closure for @a cb.
69 : */
70 : void *cb_cls;
71 :
72 : /**
73 : * Hash over the proposal data to identify the contract
74 : * which is being refunded.
75 : */
76 : struct TALER_PrivateContractHashP h_contract_terms;
77 :
78 : /**
79 : * The coin's public key. This is the value that must have been
80 : * signed (blindly) by the Exchange.
81 : */
82 : struct TALER_CoinSpendPublicKeyP coin_pub;
83 :
84 : /**
85 : * The Merchant's public key. Allows the merchant to later refund
86 : * the transaction or to inquire about the wire transfer identifier.
87 : */
88 : struct TALER_MerchantPublicKeyP merchant;
89 :
90 : /**
91 : * Merchant-generated transaction ID for the refund.
92 : */
93 : uint64_t rtransaction_id;
94 :
95 : /**
96 : * Amount to be refunded, including refund fee charged by the
97 : * exchange to the customer.
98 : */
99 : struct TALER_Amount refund_amount;
100 :
101 : };
102 :
103 :
104 : /**
105 : * Verify that the signature on the "200 OK" response
106 : * from the exchange is valid.
107 : *
108 : * @param[in,out] rh refund handle (refund fee added)
109 : * @param json json reply with the signature
110 : * @param[out] exchange_pub set to the exchange's public key
111 : * @param[out] exchange_sig set to the exchange's signature
112 : * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not
113 : */
114 : static enum GNUNET_GenericReturnValue
115 12 : verify_refund_signature_ok (struct TALER_EXCHANGE_RefundHandle *rh,
116 : const json_t *json,
117 : struct TALER_ExchangePublicKeyP *exchange_pub,
118 : struct TALER_ExchangeSignatureP *exchange_sig)
119 : {
120 : struct GNUNET_JSON_Specification spec[] = {
121 12 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
122 : exchange_sig),
123 12 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
124 : exchange_pub),
125 12 : GNUNET_JSON_spec_end ()
126 : };
127 :
128 12 : if (GNUNET_OK !=
129 12 : GNUNET_JSON_parse (json,
130 : spec,
131 : NULL, NULL))
132 : {
133 0 : GNUNET_break_op (0);
134 0 : return GNUNET_SYSERR;
135 : }
136 12 : if (GNUNET_OK !=
137 12 : TALER_EXCHANGE_test_signing_key (rh->keys,
138 : exchange_pub))
139 : {
140 0 : GNUNET_break_op (0);
141 0 : return GNUNET_SYSERR;
142 : }
143 12 : if (GNUNET_OK !=
144 12 : TALER_exchange_online_refund_confirmation_verify (
145 12 : &rh->h_contract_terms,
146 12 : &rh->coin_pub,
147 12 : &rh->merchant,
148 : rh->rtransaction_id,
149 12 : &rh->refund_amount,
150 : exchange_pub,
151 : exchange_sig))
152 : {
153 0 : GNUNET_break_op (0);
154 0 : return GNUNET_SYSERR;
155 : }
156 12 : return GNUNET_OK;
157 : }
158 :
159 :
160 : /**
161 : * Verify that the information on the "412 Dependency Failed" response
162 : * from the exchange is valid and indeed shows that there is a refund
163 : * transaction ID reuse going on.
164 : *
165 : * @param[in,out] rh refund handle (refund fee added)
166 : * @param json json reply with the signature
167 : * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not
168 : */
169 : static enum GNUNET_GenericReturnValue
170 0 : verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh,
171 : const json_t *json)
172 : {
173 : const json_t *h;
174 : json_t *e;
175 : struct GNUNET_JSON_Specification spec[] = {
176 0 : GNUNET_JSON_spec_array_const ("history",
177 : &h),
178 0 : GNUNET_JSON_spec_end ()
179 : };
180 :
181 0 : if (GNUNET_OK !=
182 0 : GNUNET_JSON_parse (json,
183 : spec,
184 : NULL, NULL))
185 : {
186 0 : GNUNET_break_op (0);
187 0 : return GNUNET_SYSERR;
188 : }
189 0 : if (1 != json_array_size (h))
190 : {
191 0 : GNUNET_break_op (0);
192 0 : return GNUNET_SYSERR;
193 : }
194 0 : e = json_array_get (h, 0);
195 : {
196 : struct TALER_Amount amount;
197 : const char *type;
198 : struct TALER_MerchantSignatureP sig;
199 : struct TALER_Amount refund_fee;
200 : struct TALER_PrivateContractHashP h_contract_terms;
201 : uint64_t rtransaction_id;
202 : struct TALER_MerchantPublicKeyP merchant_pub;
203 : struct GNUNET_JSON_Specification ispec[] = {
204 0 : TALER_JSON_spec_amount_any ("amount",
205 : &amount),
206 0 : GNUNET_JSON_spec_string ("type",
207 : &type),
208 0 : TALER_JSON_spec_amount_any ("refund_fee",
209 : &refund_fee),
210 0 : GNUNET_JSON_spec_fixed_auto ("merchant_sig",
211 : &sig),
212 0 : GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
213 : &h_contract_terms),
214 0 : GNUNET_JSON_spec_fixed_auto ("merchant_pub",
215 : &merchant_pub),
216 0 : GNUNET_JSON_spec_uint64 ("rtransaction_id",
217 : &rtransaction_id),
218 0 : GNUNET_JSON_spec_end ()
219 : };
220 :
221 0 : if (GNUNET_OK !=
222 0 : GNUNET_JSON_parse (e,
223 : ispec,
224 : NULL, NULL))
225 : {
226 0 : GNUNET_break_op (0);
227 0 : return GNUNET_SYSERR;
228 : }
229 0 : if (GNUNET_OK !=
230 0 : TALER_merchant_refund_verify (&rh->coin_pub,
231 : &h_contract_terms,
232 : rtransaction_id,
233 : &amount,
234 : &merchant_pub,
235 : &sig))
236 : {
237 0 : GNUNET_break_op (0);
238 0 : return GNUNET_SYSERR;
239 : }
240 0 : if ( (rtransaction_id != rh->rtransaction_id) ||
241 0 : (0 != GNUNET_memcmp (&rh->h_contract_terms,
242 0 : &h_contract_terms)) ||
243 0 : (0 != GNUNET_memcmp (&rh->merchant,
244 0 : &merchant_pub)) ||
245 0 : (0 == TALER_amount_cmp (&rh->refund_amount,
246 : &amount)) )
247 : {
248 0 : GNUNET_break_op (0);
249 0 : return GNUNET_SYSERR;
250 : }
251 : }
252 0 : return GNUNET_OK;
253 : }
254 :
255 :
256 : /**
257 : * Function called when we're done processing the
258 : * HTTP /refund request.
259 : *
260 : * @param cls the `struct TALER_EXCHANGE_RefundHandle`
261 : * @param response_code HTTP response code, 0 on error
262 : * @param response parsed JSON result, NULL on error
263 : */
264 : static void
265 16 : handle_refund_finished (void *cls,
266 : long response_code,
267 : const void *response)
268 : {
269 16 : struct TALER_EXCHANGE_RefundHandle *rh = cls;
270 16 : const json_t *j = response;
271 16 : struct TALER_EXCHANGE_RefundResponse rr = {
272 : .hr.reply = j,
273 16 : .hr.http_status = (unsigned int) response_code
274 : };
275 :
276 16 : rh->job = NULL;
277 16 : switch (response_code)
278 : {
279 0 : case 0:
280 0 : rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
281 0 : break;
282 12 : case MHD_HTTP_OK:
283 12 : if (GNUNET_OK !=
284 12 : verify_refund_signature_ok (rh,
285 : j,
286 : &rr.details.ok.exchange_pub,
287 : &rr.details.ok.exchange_sig))
288 : {
289 0 : GNUNET_break_op (0);
290 0 : rr.hr.http_status = 0;
291 0 : rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE;
292 : }
293 12 : break;
294 0 : case MHD_HTTP_BAD_REQUEST:
295 : /* This should never happen, either us or the exchange is buggy
296 : (or API version conflict); also can happen if the currency
297 : differs (which we should obviously never support).
298 : Just pass JSON reply to the application */
299 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
300 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
301 0 : break;
302 0 : case MHD_HTTP_FORBIDDEN:
303 : /* Nothing really to verify, exchange says one of the signatures is
304 : invalid; as we checked them, this should never happen, we
305 : should pass the JSON reply to the application */
306 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
307 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
308 0 : break;
309 0 : case MHD_HTTP_NOT_FOUND:
310 : /* Nothing really to verify, this should never
311 : happen, we should pass the JSON reply to the application */
312 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
313 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
314 0 : break;
315 2 : case MHD_HTTP_CONFLICT:
316 : /* Requested total refunds exceed deposited amount */
317 2 : rr.hr.ec = TALER_JSON_get_error_code (j);
318 2 : rr.hr.hint = TALER_JSON_get_error_hint (j);
319 2 : break;
320 2 : case MHD_HTTP_GONE:
321 : /* Kind of normal: the money was already sent to the merchant
322 : (it was too late for the refund). */
323 2 : rr.hr.ec = TALER_JSON_get_error_code (j);
324 2 : rr.hr.hint = TALER_JSON_get_error_hint (j);
325 2 : break;
326 0 : case MHD_HTTP_FAILED_DEPENDENCY:
327 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
328 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
329 0 : break;
330 0 : case MHD_HTTP_PRECONDITION_FAILED:
331 0 : if (GNUNET_OK !=
332 0 : verify_failed_dependency_ok (rh,
333 : j))
334 : {
335 0 : GNUNET_break (0);
336 0 : rr.hr.http_status = 0;
337 0 : rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE;
338 0 : rr.hr.hint = "failed precondition proof returned by exchange is invalid";
339 0 : break;
340 : }
341 : /* Two different refund requests were made about the same deposit, but
342 : carrying identical refund transaction ids. */
343 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
344 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
345 0 : break;
346 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
347 : /* Server had an internal issue; we should retry, but this API
348 : leaves this to the application */
349 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
350 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
351 0 : break;
352 0 : default:
353 : /* unexpected response code */
354 0 : GNUNET_break_op (0);
355 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
356 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
357 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
358 : "Unexpected response code %u/%d for exchange refund\n",
359 : (unsigned int) response_code,
360 : rr.hr.ec);
361 0 : break;
362 : }
363 16 : rh->cb (rh->cb_cls,
364 : &rr);
365 16 : TALER_EXCHANGE_refund_cancel (rh);
366 16 : }
367 :
368 :
369 : struct TALER_EXCHANGE_RefundHandle *
370 16 : TALER_EXCHANGE_refund (
371 : struct GNUNET_CURL_Context *ctx,
372 : const char *url,
373 : struct TALER_EXCHANGE_Keys *keys,
374 : const struct TALER_Amount *amount,
375 : const struct TALER_PrivateContractHashP *h_contract_terms,
376 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
377 : uint64_t rtransaction_id,
378 : const struct TALER_MerchantPrivateKeyP *merchant_priv,
379 : TALER_EXCHANGE_RefundCallback cb,
380 : void *cb_cls)
381 : {
382 : struct TALER_MerchantPublicKeyP merchant_pub;
383 : struct TALER_MerchantSignatureP merchant_sig;
384 : struct TALER_EXCHANGE_RefundHandle *rh;
385 : json_t *refund_obj;
386 : CURL *eh;
387 : char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
388 :
389 16 : GNUNET_CRYPTO_eddsa_key_get_public (&merchant_priv->eddsa_priv,
390 : &merchant_pub.eddsa_pub);
391 16 : TALER_merchant_refund_sign (coin_pub,
392 : h_contract_terms,
393 : rtransaction_id,
394 : amount,
395 : merchant_priv,
396 : &merchant_sig);
397 : {
398 : char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
399 : char *end;
400 :
401 16 : end = GNUNET_STRINGS_data_to_string (
402 : coin_pub,
403 : sizeof (struct TALER_CoinSpendPublicKeyP),
404 : pub_str,
405 : sizeof (pub_str));
406 16 : *end = '\0';
407 16 : GNUNET_snprintf (arg_str,
408 : sizeof (arg_str),
409 : "coins/%s/refund",
410 : pub_str);
411 : }
412 16 : refund_obj = GNUNET_JSON_PACK (
413 : TALER_JSON_pack_amount ("refund_amount",
414 : amount),
415 : GNUNET_JSON_pack_data_auto ("h_contract_terms",
416 : h_contract_terms),
417 : GNUNET_JSON_pack_uint64 ("rtransaction_id",
418 : rtransaction_id),
419 : GNUNET_JSON_pack_data_auto ("merchant_pub",
420 : &merchant_pub),
421 : GNUNET_JSON_pack_data_auto ("merchant_sig",
422 : &merchant_sig));
423 16 : rh = GNUNET_new (struct TALER_EXCHANGE_RefundHandle);
424 16 : rh->cb = cb;
425 16 : rh->cb_cls = cb_cls;
426 16 : rh->url = TALER_url_join (url,
427 : arg_str,
428 : NULL);
429 16 : if (NULL == rh->url)
430 : {
431 0 : json_decref (refund_obj);
432 0 : GNUNET_free (rh);
433 0 : return NULL;
434 : }
435 16 : rh->h_contract_terms = *h_contract_terms;
436 16 : rh->coin_pub = *coin_pub;
437 16 : rh->merchant = merchant_pub;
438 16 : rh->rtransaction_id = rtransaction_id;
439 16 : rh->refund_amount = *amount;
440 16 : eh = TALER_EXCHANGE_curl_easy_get_ (rh->url);
441 32 : if ( (NULL == eh) ||
442 : (GNUNET_OK !=
443 16 : TALER_curl_easy_post (&rh->ctx,
444 : eh,
445 : refund_obj)) )
446 : {
447 0 : GNUNET_break (0);
448 0 : if (NULL != eh)
449 0 : curl_easy_cleanup (eh);
450 0 : json_decref (refund_obj);
451 0 : GNUNET_free (rh->url);
452 0 : GNUNET_free (rh);
453 0 : return NULL;
454 : }
455 16 : json_decref (refund_obj);
456 16 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
457 : "URL for refund: `%s'\n",
458 : rh->url);
459 16 : rh->keys = TALER_EXCHANGE_keys_incref (keys);
460 32 : rh->job = GNUNET_CURL_job_add2 (ctx,
461 : eh,
462 16 : rh->ctx.headers,
463 : &handle_refund_finished,
464 : rh);
465 16 : return rh;
466 : }
467 :
468 :
469 : void
470 16 : TALER_EXCHANGE_refund_cancel (struct TALER_EXCHANGE_RefundHandle *refund)
471 : {
472 16 : if (NULL != refund->job)
473 : {
474 0 : GNUNET_CURL_job_cancel (refund->job);
475 0 : refund->job = NULL;
476 : }
477 16 : GNUNET_free (refund->url);
478 16 : TALER_curl_easy_post_finished (&refund->ctx);
479 16 : TALER_EXCHANGE_keys_decref (refund->keys);
480 16 : GNUNET_free (refund);
481 16 : }
482 :
483 :
484 : /* end of exchange_api_refund.c */
|