exchange

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

exchange_api_purse_deposit.c (15552B)


      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_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 "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_common.h"
     32 #include "exchange_api_handle.h"
     33 #include "taler/taler_signatures.h"
     34 #include "exchange_api_curl_defaults.h"
     35 
     36 
     37 /**
     38  * Information we track per coin.
     39  */
     40 struct Coin
     41 {
     42   /**
     43    * Coin's public key.
     44    */
     45   struct TALER_CoinSpendPublicKeyP coin_pub;
     46 
     47   /**
     48    * Signature made with the coin.
     49    */
     50   struct TALER_CoinSpendSignatureP coin_sig;
     51 
     52   /**
     53    * Coin's denomination.
     54    */
     55   struct TALER_DenominationHashP h_denom_pub;
     56 
     57   /**
     58    * Age restriction hash for the coin.
     59    */
     60   struct TALER_AgeCommitmentHashP ahac;
     61 
     62   /**
     63    * How much did we say the coin contributed.
     64    */
     65   struct TALER_Amount contribution;
     66 };
     67 
     68 
     69 /**
     70  * @brief A purse create with deposit handle
     71  */
     72 struct TALER_EXCHANGE_PurseDepositHandle
     73 {
     74 
     75   /**
     76    * The keys of the exchange this request handle will use
     77    */
     78   struct TALER_EXCHANGE_Keys *keys;
     79 
     80   /**
     81    * The url for this request.
     82    */
     83   char *url;
     84 
     85   /**
     86    * The base url of the exchange we are talking to.
     87    */
     88   char *base_url;
     89 
     90   /**
     91    * Context for #TEH_curl_easy_post(). Keeps the data that must
     92    * persist for Curl to make the upload.
     93    */
     94   struct TALER_CURL_PostContext ctx;
     95 
     96   /**
     97    * Handle for the request.
     98    */
     99   struct GNUNET_CURL_Job *job;
    100 
    101   /**
    102    * Function to call with the result.
    103    */
    104   TALER_EXCHANGE_PurseDepositCallback cb;
    105 
    106   /**
    107    * Closure for @a cb.
    108    */
    109   void *cb_cls;
    110 
    111   /**
    112    * Public key of the purse.
    113    */
    114   struct TALER_PurseContractPublicKeyP purse_pub;
    115 
    116   /**
    117    * Array of @e num_deposits coins we are depositing.
    118    */
    119   struct Coin *coins;
    120 
    121   /**
    122    * Number of coins we are depositing.
    123    */
    124   unsigned int num_deposits;
    125 };
    126 
    127 
    128 /**
    129  * Function called when we're done processing the
    130  * HTTP /purses/$PID/deposit request.
    131  *
    132  * @param cls the `struct TALER_EXCHANGE_PurseDepositHandle`
    133  * @param response_code HTTP response code, 0 on error
    134  * @param response parsed JSON result, NULL on error
    135  */
    136 static void
    137 handle_purse_deposit_finished (void *cls,
    138                                long response_code,
    139                                const void *response)
    140 {
    141   struct TALER_EXCHANGE_PurseDepositHandle *pch = cls;
    142   const json_t *j = response;
    143   struct TALER_EXCHANGE_PurseDepositResponse dr = {
    144     .hr.reply = j,
    145     .hr.http_status = (unsigned int) response_code
    146   };
    147   const struct TALER_EXCHANGE_Keys *keys = pch->keys;
    148 
    149   pch->job = NULL;
    150   switch (response_code)
    151   {
    152   case 0:
    153     dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    154     break;
    155   case MHD_HTTP_OK:
    156     {
    157       struct GNUNET_TIME_Timestamp etime;
    158       struct TALER_ExchangeSignatureP exchange_sig;
    159       struct TALER_ExchangePublicKeyP exchange_pub;
    160       struct GNUNET_JSON_Specification spec[] = {
    161         GNUNET_JSON_spec_fixed_auto ("exchange_sig",
    162                                      &exchange_sig),
    163         GNUNET_JSON_spec_fixed_auto ("exchange_pub",
    164                                      &exchange_pub),
    165         GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
    166                                      &dr.details.ok.h_contract_terms),
    167         GNUNET_JSON_spec_timestamp ("exchange_timestamp",
    168                                     &etime),
    169         GNUNET_JSON_spec_timestamp ("purse_expiration",
    170                                     &dr.details.ok.purse_expiration),
    171         TALER_JSON_spec_amount ("total_deposited",
    172                                 keys->currency,
    173                                 &dr.details.ok.total_deposited),
    174         TALER_JSON_spec_amount ("purse_value_after_fees",
    175                                 keys->currency,
    176                                 &dr.details.ok.purse_value_after_fees),
    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 (keys,
    192                                            &exchange_pub))
    193       {
    194         GNUNET_break_op (0);
    195         dr.hr.http_status = 0;
    196         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
    197         break;
    198       }
    199       if (GNUNET_OK !=
    200           TALER_exchange_online_purse_created_verify (
    201             etime,
    202             dr.details.ok.purse_expiration,
    203             &dr.details.ok.purse_value_after_fees,
    204             &dr.details.ok.total_deposited,
    205             &pch->purse_pub,
    206             &dr.details.ok.h_contract_terms,
    207             &exchange_pub,
    208             &exchange_sig))
    209       {
    210         GNUNET_break_op (0);
    211         dr.hr.http_status = 0;
    212         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
    213         break;
    214       }
    215     }
    216     break;
    217   case MHD_HTTP_BAD_REQUEST:
    218     /* This should never happen, either us or the exchange is buggy
    219        (or API version conflict); just pass JSON reply to the application */
    220     dr.hr.ec = TALER_JSON_get_error_code (j);
    221     break;
    222   case MHD_HTTP_FORBIDDEN:
    223     dr.hr.ec = TALER_JSON_get_error_code (j);
    224     /* Nothing really to verify, exchange says one of the signatures is
    225        invalid; as we checked them, this should never happen, we
    226        should pass the JSON reply to the application */
    227     break;
    228   case MHD_HTTP_NOT_FOUND:
    229     dr.hr.ec = TALER_JSON_get_error_code (j);
    230     /* Nothing really to verify, this should never
    231        happen, we should pass the JSON reply to the application */
    232     break;
    233   case MHD_HTTP_CONFLICT:
    234     dr.hr.ec = TALER_JSON_get_error_code (j);
    235     switch (dr.hr.ec)
    236     {
    237     case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA:
    238       {
    239         struct TALER_CoinSpendPublicKeyP coin_pub;
    240         struct TALER_CoinSpendSignatureP coin_sig;
    241         struct TALER_DenominationHashP h_denom_pub;
    242         struct TALER_AgeCommitmentHashP phac;
    243         bool found = false;
    244 
    245         if (GNUNET_OK !=
    246             TALER_EXCHANGE_check_purse_coin_conflict_ (
    247               &pch->purse_pub,
    248               pch->base_url,
    249               j,
    250               &h_denom_pub,
    251               &phac,
    252               &coin_pub,
    253               &coin_sig))
    254         {
    255           GNUNET_break_op (0);
    256           dr.hr.http_status = 0;
    257           dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    258           break;
    259         }
    260         for (unsigned int i = 0; i<pch->num_deposits; i++)
    261         {
    262           struct Coin *coin = &pch->coins[i];
    263           if (0 != GNUNET_memcmp (&coin_pub,
    264                                   &coin->coin_pub))
    265             continue;
    266           if (0 !=
    267               GNUNET_memcmp (&coin->h_denom_pub,
    268                              &h_denom_pub))
    269           {
    270             found = true;
    271             break;
    272           }
    273           if (0 !=
    274               GNUNET_memcmp (&coin->ahac,
    275                              &phac))
    276           {
    277             found = true;
    278             break;
    279           }
    280           if (0 == GNUNET_memcmp (&coin_sig,
    281                                   &coin->coin_sig))
    282           {
    283             /* identical signature => not a conflict */
    284             continue;
    285           }
    286           found = true;
    287           break;
    288         }
    289         if (! found)
    290         {
    291           GNUNET_break_op (0);
    292           dr.hr.http_status = 0;
    293           dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    294           break;
    295         }
    296         /* meta data conflict is real! */
    297         break;
    298       }
    299     case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
    300       /* Nothing to check anymore here, proof needs to be
    301          checked in the GET /coins/$COIN_PUB handler */
    302       break;
    303     case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
    304       break;
    305     default:
    306       GNUNET_break_op (0);
    307       dr.hr.http_status = 0;
    308       dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    309       break;
    310     } /* ec switch */
    311     break;
    312   case MHD_HTTP_GONE:
    313     /* could happen if denomination was revoked or purse expired */
    314     /* Note: one might want to check /keys for revocation
    315        signature here, alas tricky in case our /keys
    316        is outdated => left to clients */
    317     dr.hr.ec = TALER_JSON_get_error_code (j);
    318     break;
    319   case MHD_HTTP_INTERNAL_SERVER_ERROR:
    320     dr.hr.ec = TALER_JSON_get_error_code (j);
    321     /* Server had an internal issue; we should retry, but this API
    322        leaves this to the application */
    323     break;
    324   default:
    325     /* unexpected response code */
    326     dr.hr.ec = TALER_JSON_get_error_code (j);
    327     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    328                 "Unexpected response code %u/%d for exchange deposit\n",
    329                 (unsigned int) response_code,
    330                 dr.hr.ec);
    331     GNUNET_break_op (0);
    332     break;
    333   }
    334   if (TALER_EC_NONE == dr.hr.ec)
    335     dr.hr.hint = NULL;
    336   else
    337     dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec);
    338   pch->cb (pch->cb_cls,
    339            &dr);
    340   TALER_EXCHANGE_purse_deposit_cancel (pch);
    341 }
    342 
    343 
    344 struct TALER_EXCHANGE_PurseDepositHandle *
    345 TALER_EXCHANGE_purse_deposit (
    346   struct GNUNET_CURL_Context *ctx,
    347   const char *url,
    348   struct TALER_EXCHANGE_Keys *keys,
    349   const char *purse_exchange_url,
    350   const struct TALER_PurseContractPublicKeyP *purse_pub,
    351   uint8_t min_age,
    352   unsigned int num_deposits,
    353   const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits],
    354   TALER_EXCHANGE_PurseDepositCallback cb,
    355   void *cb_cls)
    356 {
    357   struct TALER_EXCHANGE_PurseDepositHandle *pch;
    358   json_t *create_obj;
    359   json_t *deposit_arr;
    360   CURL *eh;
    361   char arg_str[sizeof (pch->purse_pub) * 2 + 32];
    362 
    363   // FIXME: use purse_exchange_url for wad transfers (#7271)
    364   (void) purse_exchange_url;
    365   if (0 == num_deposits)
    366   {
    367     GNUNET_break (0);
    368     return NULL;
    369   }
    370   pch = GNUNET_new (struct TALER_EXCHANGE_PurseDepositHandle);
    371   pch->purse_pub = *purse_pub;
    372   pch->cb = cb;
    373   pch->cb_cls = cb_cls;
    374   {
    375     char pub_str[sizeof (pch->purse_pub) * 2];
    376     char *end;
    377 
    378     end = GNUNET_STRINGS_data_to_string (
    379       &pch->purse_pub,
    380       sizeof (pch->purse_pub),
    381       pub_str,
    382       sizeof (pub_str));
    383     *end = '\0';
    384     GNUNET_snprintf (arg_str,
    385                      sizeof (arg_str),
    386                      "purses/%s/deposit",
    387                      pub_str);
    388   }
    389   pch->url = TALER_url_join (url,
    390                              arg_str,
    391                              NULL);
    392   if (NULL == pch->url)
    393   {
    394     GNUNET_break (0);
    395     GNUNET_free (pch);
    396     return NULL;
    397   }
    398   deposit_arr = json_array ();
    399   GNUNET_assert (NULL != deposit_arr);
    400   pch->base_url = GNUNET_strdup (url);
    401   pch->num_deposits = num_deposits;
    402   pch->coins = GNUNET_new_array (num_deposits,
    403                                  struct Coin);
    404   for (unsigned int i = 0; i<num_deposits; i++)
    405   {
    406     const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i];
    407     const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof;
    408     struct Coin *coin = &pch->coins[i];
    409     json_t *jdeposit;
    410     struct TALER_AgeCommitmentHashP *achp = NULL;
    411     struct TALER_AgeAttestationP attest;
    412     struct TALER_AgeAttestationP *attestp = NULL;
    413 
    414     if (NULL != acp)
    415     {
    416       TALER_age_commitment_hash (&acp->commitment,
    417                                  &coin->ahac);
    418       achp = &coin->ahac;
    419       if (GNUNET_OK !=
    420           TALER_age_commitment_attest (acp,
    421                                        min_age,
    422                                        &attest))
    423       {
    424         GNUNET_break (0);
    425         json_decref (deposit_arr);
    426         GNUNET_free (pch->base_url);
    427         GNUNET_free (pch->coins);
    428         GNUNET_free (pch->url);
    429         GNUNET_free (pch);
    430         return NULL;
    431       }
    432       attestp = &attest;
    433     }
    434     GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv,
    435                                         &coin->coin_pub.eddsa_pub);
    436     coin->h_denom_pub = deposit->h_denom_pub;
    437     coin->contribution = deposit->amount;
    438     TALER_wallet_purse_deposit_sign (
    439       pch->base_url,
    440       &pch->purse_pub,
    441       &deposit->amount,
    442       &coin->h_denom_pub,
    443       &coin->ahac,
    444       &deposit->coin_priv,
    445       &coin->coin_sig);
    446     jdeposit = GNUNET_JSON_PACK (
    447       GNUNET_JSON_pack_allow_null (
    448         GNUNET_JSON_pack_data_auto ("h_age_commitment",
    449                                     achp)),
    450       GNUNET_JSON_pack_allow_null (
    451         GNUNET_JSON_pack_data_auto ("age_attestation",
    452                                     attestp)),
    453       TALER_JSON_pack_amount ("amount",
    454                               &deposit->amount),
    455       GNUNET_JSON_pack_data_auto ("denom_pub_hash",
    456                                   &deposit->h_denom_pub),
    457       TALER_JSON_pack_denom_sig ("ub_sig",
    458                                  &deposit->denom_sig),
    459       GNUNET_JSON_pack_data_auto ("coin_pub",
    460                                   &coin->coin_pub),
    461       GNUNET_JSON_pack_data_auto ("coin_sig",
    462                                   &coin->coin_sig));
    463     GNUNET_assert (0 ==
    464                    json_array_append_new (deposit_arr,
    465                                           jdeposit));
    466   }
    467   create_obj = GNUNET_JSON_PACK (
    468     GNUNET_JSON_pack_array_steal ("deposits",
    469                                   deposit_arr));
    470   GNUNET_assert (NULL != create_obj);
    471   eh = TALER_EXCHANGE_curl_easy_get_ (pch->url);
    472   if ( (NULL == eh) ||
    473        (GNUNET_OK !=
    474         TALER_curl_easy_post (&pch->ctx,
    475                               eh,
    476                               create_obj)) )
    477   {
    478     GNUNET_break (0);
    479     if (NULL != eh)
    480       curl_easy_cleanup (eh);
    481     json_decref (create_obj);
    482     GNUNET_free (pch->base_url);
    483     GNUNET_free (pch->url);
    484     GNUNET_free (pch->coins);
    485     GNUNET_free (pch);
    486     return NULL;
    487   }
    488   json_decref (create_obj);
    489   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    490               "URL for purse deposit: `%s'\n",
    491               pch->url);
    492   pch->keys = TALER_EXCHANGE_keys_incref (keys);
    493   pch->job = GNUNET_CURL_job_add2 (ctx,
    494                                    eh,
    495                                    pch->ctx.headers,
    496                                    &handle_purse_deposit_finished,
    497                                    pch);
    498   return pch;
    499 }
    500 
    501 
    502 void
    503 TALER_EXCHANGE_purse_deposit_cancel (
    504   struct TALER_EXCHANGE_PurseDepositHandle *pch)
    505 {
    506   if (NULL != pch->job)
    507   {
    508     GNUNET_CURL_job_cancel (pch->job);
    509     pch->job = NULL;
    510   }
    511   GNUNET_free (pch->base_url);
    512   GNUNET_free (pch->url);
    513   GNUNET_free (pch->coins);
    514   TALER_EXCHANGE_keys_decref (pch->keys);
    515   TALER_curl_easy_post_finished (&pch->ctx);
    516   GNUNET_free (pch);
    517 }
    518 
    519 
    520 /* end of exchange_api_purse_deposit.c */