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-deposit.c (16162B)


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