exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

exchange_api_post-purses-PURSE_PUB-merge.c (14751B)


      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_post-purses-PURSE_PUB-merge.c
     19  * @brief Implementation of the client to merge a purse
     20  *        into an account
     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_handle.h"
     32 #include "exchange_api_common.h"
     33 #include "taler/taler_signatures.h"
     34 #include "exchange_api_curl_defaults.h"
     35 
     36 
     37 /**
     38  * @brief A purse merge with deposit handle
     39  */
     40 struct TALER_EXCHANGE_PostPursesMergeHandle
     41 {
     42 
     43   /**
     44    * The curl context for this request.
     45    */
     46   struct GNUNET_CURL_Context *ctx;
     47 
     48   /**
     49    * The base URL of the exchange.
     50    */
     51   char *base_url;
     52 
     53   /**
     54    * The keys of the exchange this request handle will use
     55    */
     56   struct TALER_EXCHANGE_Keys *keys;
     57 
     58   /**
     59    * The url for this request, set during _start.
     60    */
     61   char *url;
     62 
     63   /**
     64    * Context for #TEH_curl_easy_post(). Keeps the data that must
     65    * persist for Curl to make the upload.
     66    */
     67   struct TALER_CURL_PostContext post_ctx;
     68 
     69   /**
     70    * Handle for the request.
     71    */
     72   struct GNUNET_CURL_Job *job;
     73 
     74   /**
     75    * Function to call with the result.
     76    */
     77   TALER_EXCHANGE_PostPursesMergeCallback cb;
     78 
     79   /**
     80    * Closure for @a cb.
     81    */
     82   TALER_EXCHANGE_POST_PURSES_MERGE_RESULT_CLOSURE *cb_cls;
     83 
     84   /**
     85    * Base URL of the provider hosting the @e reserve_pub.
     86    */
     87   char *provider_url;
     88 
     89   /**
     90    * Signature for our operation.
     91    */
     92   struct TALER_PurseMergeSignatureP merge_sig;
     93 
     94   /**
     95    * Expected value in the purse after fees.
     96    */
     97   struct TALER_Amount purse_value_after_fees;
     98 
     99   /**
    100    * Public key of the reserve public key.
    101    */
    102   struct TALER_ReservePublicKeyP reserve_pub;
    103 
    104   /**
    105    * Public key of the purse.
    106    */
    107   struct TALER_PurseContractPublicKeyP purse_pub;
    108 
    109   /**
    110    * Hash over the purse's contract terms.
    111    */
    112   struct TALER_PrivateContractHashP h_contract_terms;
    113 
    114   /**
    115    * When does the purse expire.
    116    */
    117   struct GNUNET_TIME_Timestamp purse_expiration;
    118 
    119   /**
    120    * Our merge key.
    121    */
    122   struct TALER_PurseMergePrivateKeyP merge_priv;
    123 
    124   /**
    125    * Reserve signature affirming the merge.
    126    */
    127   struct TALER_ReserveSignatureP reserve_sig;
    128 
    129   /**
    130    * The JSON body to post, built during _create and posted during _start.
    131    */
    132   json_t *body;
    133 
    134 };
    135 
    136 
    137 /**
    138  * Function called when we're done processing the
    139  * HTTP /purse/$PID/merge request.
    140  *
    141  * @param cls the `struct TALER_EXCHANGE_PostPursesMergeHandle`
    142  * @param response_code HTTP response code, 0 on error
    143  * @param response parsed JSON result, NULL on error
    144  */
    145 static void
    146 handle_purse_merge_finished (void *cls,
    147                              long response_code,
    148                              const void *response)
    149 {
    150   struct TALER_EXCHANGE_PostPursesMergeHandle *pch = cls;
    151   const json_t *j = response;
    152   struct TALER_EXCHANGE_PostPursesMergeResponse dr = {
    153     .hr.reply = j,
    154     .hr.http_status = (unsigned int) response_code,
    155     .reserve_sig = &pch->reserve_sig
    156   };
    157 
    158   pch->job = NULL;
    159   switch (response_code)
    160   {
    161   case 0:
    162     dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    163     break;
    164   case MHD_HTTP_OK:
    165     {
    166       struct TALER_Amount total_deposited;
    167       struct GNUNET_JSON_Specification spec[] = {
    168         GNUNET_JSON_spec_fixed_auto ("exchange_sig",
    169                                      &dr.details.ok.exchange_sig),
    170         GNUNET_JSON_spec_fixed_auto ("exchange_pub",
    171                                      &dr.details.ok.exchange_pub),
    172         GNUNET_JSON_spec_timestamp ("exchange_timestamp",
    173                                     &dr.details.ok.etime),
    174         TALER_JSON_spec_amount ("merge_amount",
    175                                 pch->purse_value_after_fees.currency,
    176                                 &total_deposited),
    177         GNUNET_JSON_spec_end ()
    178       };
    179 
    180       if (GNUNET_OK !=
    181           GNUNET_JSON_parse (j,
    182                              spec,
    183                              NULL, NULL))
    184       {
    185         GNUNET_break_op (0);
    186         dr.hr.http_status = 0;
    187         dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    188         break;
    189       }
    190       if (GNUNET_OK !=
    191           TALER_EXCHANGE_test_signing_key (pch->keys,
    192                                            &dr.details.ok.exchange_pub))
    193       {
    194         GNUNET_break_op (0);
    195         dr.hr.http_status = 0;
    196         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID;
    197         break;
    198       }
    199       if (GNUNET_OK !=
    200           TALER_exchange_online_purse_merged_verify (
    201             dr.details.ok.etime,
    202             pch->purse_expiration,
    203             &pch->purse_value_after_fees,
    204             &pch->purse_pub,
    205             &pch->h_contract_terms,
    206             &pch->reserve_pub,
    207             pch->provider_url,
    208             &dr.details.ok.exchange_pub,
    209             &dr.details.ok.exchange_sig))
    210       {
    211         GNUNET_break_op (0);
    212         dr.hr.http_status = 0;
    213         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID;
    214         break;
    215       }
    216     }
    217     break;
    218   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     dr.hr.ec = TALER_JSON_get_error_code (j);
    222     dr.hr.hint = TALER_JSON_get_error_hint (j);
    223     break;
    224   case MHD_HTTP_PAYMENT_REQUIRED:
    225     /* purse was not (yet) full */
    226     dr.hr.ec = TALER_JSON_get_error_code (j);
    227     dr.hr.hint = TALER_JSON_get_error_hint (j);
    228     break;
    229   case MHD_HTTP_FORBIDDEN:
    230     dr.hr.ec = TALER_JSON_get_error_code (j);
    231     dr.hr.hint = TALER_JSON_get_error_hint (j);
    232     /* Nothing really to verify, exchange says one of the signatures is
    233        invalid; as we checked them, this should never happen, we
    234        should pass the JSON reply to the application */
    235     break;
    236   case MHD_HTTP_NOT_FOUND:
    237     dr.hr.ec = TALER_JSON_get_error_code (j);
    238     dr.hr.hint = TALER_JSON_get_error_hint (j);
    239     /* Nothing really to verify, this should never
    240        happen, we should pass the JSON reply to the application */
    241     break;
    242   case MHD_HTTP_CONFLICT:
    243     {
    244       struct TALER_PurseMergePublicKeyP merge_pub;
    245 
    246       GNUNET_CRYPTO_eddsa_key_get_public (&pch->merge_priv.eddsa_priv,
    247                                           &merge_pub.eddsa_pub);
    248       if (GNUNET_OK !=
    249           TALER_EXCHANGE_check_purse_merge_conflict_ (
    250             &pch->merge_sig,
    251             &merge_pub,
    252             &pch->purse_pub,
    253             pch->provider_url,
    254             j))
    255       {
    256         GNUNET_break_op (0);
    257         dr.hr.http_status = 0;
    258         dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    259         break;
    260       }
    261       break;
    262     }
    263     break;
    264   case MHD_HTTP_GONE:
    265     /* could happen if denomination was revoked */
    266     /* Note: one might want to check /keys for revocation
    267        signature here, alas tricky in case our /keys
    268        is outdated => left to clients */
    269     dr.hr.ec = TALER_JSON_get_error_code (j);
    270     dr.hr.hint = TALER_JSON_get_error_hint (j);
    271     break;
    272   case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
    273     {
    274       struct GNUNET_JSON_Specification spec[] = {
    275         GNUNET_JSON_spec_uint64 (
    276           "requirement_row",
    277           &dr.details.unavailable_for_legal_reasons.requirement_row),
    278         GNUNET_JSON_spec_end ()
    279       };
    280 
    281       if (GNUNET_OK !=
    282           GNUNET_JSON_parse (j,
    283                              spec,
    284                              NULL, NULL))
    285       {
    286         GNUNET_break_op (0);
    287         dr.hr.http_status = 0;
    288         dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    289         break;
    290       }
    291     }
    292     break;
    293   case MHD_HTTP_INTERNAL_SERVER_ERROR:
    294     dr.hr.ec = TALER_JSON_get_error_code (j);
    295     dr.hr.hint = TALER_JSON_get_error_hint (j);
    296     /* Server had an internal issue; we should retry, but this API
    297        leaves this to the application */
    298     break;
    299   default:
    300     /* unexpected response code */
    301     dr.hr.ec = TALER_JSON_get_error_code (j);
    302     dr.hr.hint = TALER_JSON_get_error_hint (j);
    303     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    304                 "Unexpected response code %u/%d for exchange deposit\n",
    305                 (unsigned int) response_code,
    306                 dr.hr.ec);
    307     GNUNET_break_op (0);
    308     break;
    309   }
    310   if (NULL != pch->cb)
    311   {
    312     pch->cb (pch->cb_cls,
    313              &dr);
    314     pch->cb = NULL;
    315   }
    316   TALER_EXCHANGE_post_purses_merge_cancel (pch);
    317 }
    318 
    319 
    320 struct TALER_EXCHANGE_PostPursesMergeHandle *
    321 TALER_EXCHANGE_post_purses_merge_create (
    322   struct GNUNET_CURL_Context *ctx,
    323   const char *url,
    324   struct TALER_EXCHANGE_Keys *keys,
    325   const char *reserve_exchange_url,
    326   const struct TALER_ReservePrivateKeyP *reserve_priv,
    327   const struct TALER_PurseContractPublicKeyP *purse_pub,
    328   const struct TALER_PurseMergePrivateKeyP *merge_priv,
    329   const struct TALER_PrivateContractHashP *h_contract_terms,
    330   uint8_t min_age,
    331   const struct TALER_Amount *purse_value_after_fees,
    332   struct GNUNET_TIME_Timestamp purse_expiration,
    333   struct GNUNET_TIME_Timestamp merge_timestamp)
    334 {
    335   struct TALER_EXCHANGE_PostPursesMergeHandle *pch;
    336   struct TALER_NormalizedPayto reserve_url;
    337 
    338   pch = GNUNET_new (struct TALER_EXCHANGE_PostPursesMergeHandle);
    339   pch->ctx = ctx;
    340   pch->base_url = GNUNET_strdup (url);
    341   pch->merge_priv = *merge_priv;
    342   pch->purse_pub = *purse_pub;
    343   pch->h_contract_terms = *h_contract_terms;
    344   pch->purse_expiration = purse_expiration;
    345   pch->purse_value_after_fees = *purse_value_after_fees;
    346   if (NULL == reserve_exchange_url)
    347     pch->provider_url = GNUNET_strdup (url);
    348   else
    349     pch->provider_url = GNUNET_strdup (reserve_exchange_url);
    350   GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
    351                                       &pch->reserve_pub.eddsa_pub);
    352 
    353   reserve_url = TALER_reserve_make_payto (pch->provider_url,
    354                                           &pch->reserve_pub);
    355   if (NULL == reserve_url.normalized_payto)
    356   {
    357     GNUNET_break (0);
    358     GNUNET_free (pch->provider_url);
    359     GNUNET_free (pch->base_url);
    360     GNUNET_free (pch);
    361     return NULL;
    362   }
    363   TALER_wallet_purse_merge_sign (reserve_url,
    364                                  merge_timestamp,
    365                                  purse_pub,
    366                                  merge_priv,
    367                                  &pch->merge_sig);
    368   {
    369     struct TALER_Amount zero_purse_fee;
    370 
    371     GNUNET_assert (GNUNET_OK ==
    372                    TALER_amount_set_zero (purse_value_after_fees->currency,
    373                                           &zero_purse_fee));
    374     TALER_wallet_account_merge_sign (merge_timestamp,
    375                                      purse_pub,
    376                                      purse_expiration,
    377                                      h_contract_terms,
    378                                      purse_value_after_fees,
    379                                      &zero_purse_fee,
    380                                      min_age,
    381                                      TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE,
    382                                      reserve_priv,
    383                                      &pch->reserve_sig);
    384   }
    385   // FIXME: move JSON construction into _start() function
    386   pch->body = GNUNET_JSON_PACK (
    387     TALER_JSON_pack_normalized_payto ("payto_uri",
    388                                       reserve_url),
    389     GNUNET_JSON_pack_data_auto ("merge_sig",
    390                                 &pch->merge_sig),
    391     GNUNET_JSON_pack_data_auto ("reserve_sig",
    392                                 &pch->reserve_sig),
    393     GNUNET_JSON_pack_timestamp ("merge_timestamp",
    394                                 merge_timestamp));
    395   GNUNET_free (reserve_url.normalized_payto);
    396   if (NULL == pch->body)
    397   {
    398     GNUNET_break (0);
    399     GNUNET_free (pch->provider_url);
    400     GNUNET_free (pch->base_url);
    401     GNUNET_free (pch);
    402     return NULL;
    403   }
    404   pch->keys = TALER_EXCHANGE_keys_incref (keys);
    405   return pch;
    406 }
    407 
    408 
    409 enum TALER_ErrorCode
    410 TALER_EXCHANGE_post_purses_merge_start (
    411   struct TALER_EXCHANGE_PostPursesMergeHandle *pch,
    412   TALER_EXCHANGE_PostPursesMergeCallback cb,
    413   TALER_EXCHANGE_POST_PURSES_MERGE_RESULT_CLOSURE *cb_cls)
    414 {
    415   char arg_str[sizeof (pch->purse_pub) * 2 + 32];
    416   CURL *eh;
    417 
    418   pch->cb = cb;
    419   pch->cb_cls = cb_cls;
    420 
    421   {
    422     char pub_str[sizeof (pch->purse_pub) * 2];
    423     char *end;
    424 
    425     end = GNUNET_STRINGS_data_to_string (
    426       &pch->purse_pub,
    427       sizeof (pch->purse_pub),
    428       pub_str,
    429       sizeof (pub_str));
    430     *end = '\0';
    431     GNUNET_snprintf (arg_str,
    432                      sizeof (arg_str),
    433                      "purses/%s/merge",
    434                      pub_str);
    435   }
    436   pch->url = TALER_url_join (pch->base_url,
    437                              arg_str,
    438                              NULL);
    439   if (NULL == pch->url)
    440   {
    441     GNUNET_break (0);
    442     return TALER_EC_GENERIC_CONFIGURATION_INVALID;
    443   }
    444   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    445               "URL for purse merge: `%s'\n",
    446               pch->url);
    447   eh = TALER_EXCHANGE_curl_easy_get_ (pch->url);
    448   if ( (NULL == eh) ||
    449        (GNUNET_OK !=
    450         TALER_curl_easy_post (&pch->post_ctx,
    451                               eh,
    452                               pch->body)) )
    453   {
    454     GNUNET_break (0);
    455     if (NULL != eh)
    456       curl_easy_cleanup (eh);
    457     return TALER_EC_GENERIC_CURL_ALLOCATION_FAILURE;
    458   }
    459   pch->job = GNUNET_CURL_job_add2 (pch->ctx,
    460                                    eh,
    461                                    pch->post_ctx.headers,
    462                                    &handle_purse_merge_finished,
    463                                    pch);
    464   if (NULL == pch->job)
    465     return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    466   return TALER_EC_NONE;
    467 }
    468 
    469 
    470 void
    471 TALER_EXCHANGE_post_purses_merge_cancel (
    472   struct TALER_EXCHANGE_PostPursesMergeHandle *pch)
    473 {
    474   if (NULL != pch->job)
    475   {
    476     GNUNET_CURL_job_cancel (pch->job);
    477     pch->job = NULL;
    478   }
    479   GNUNET_free (pch->url);
    480   GNUNET_free (pch->base_url);
    481   GNUNET_free (pch->provider_url);
    482   TALER_curl_easy_post_finished (&pch->post_ctx);
    483   TALER_EXCHANGE_keys_decref (pch->keys);
    484   json_decref (pch->body);
    485   GNUNET_free (pch);
    486 }
    487 
    488 
    489 /* end of exchange_api_post-purses-PURSE_PUB-merge.c */