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 (16128B)


      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 GNUNET_JSON_Specification spec[] = {
    169         GNUNET_JSON_spec_fixed_auto ("exchange_sig",
    170                                      &dr.details.ok.exchange_sig),
    171         GNUNET_JSON_spec_fixed_auto ("exchange_pub",
    172                                      &dr.details.ok.exchange_pub),
    173         GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
    174                                      &dr.details.ok.h_contract_terms),
    175         GNUNET_JSON_spec_timestamp ("exchange_timestamp",
    176                                     &etime),
    177         GNUNET_JSON_spec_timestamp ("purse_expiration",
    178                                     &dr.details.ok.purse_expiration),
    179         TALER_JSON_spec_amount ("total_deposited",
    180                                 keys->currency,
    181                                 &dr.details.ok.total_deposited),
    182         TALER_JSON_spec_amount ("purse_value_after_fees",
    183                                 keys->currency,
    184                                 &dr.details.ok.purse_value_after_fees),
    185         GNUNET_JSON_spec_end ()
    186       };
    187 
    188       if (GNUNET_OK !=
    189           GNUNET_JSON_parse (j,
    190                              spec,
    191                              NULL, NULL))
    192       {
    193         GNUNET_break_op (0);
    194         dr.hr.http_status = 0;
    195         dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    196         break;
    197       }
    198       if (GNUNET_OK !=
    199           TALER_EXCHANGE_test_signing_key (keys,
    200                                            &dr.details.ok.exchange_pub))
    201       {
    202         GNUNET_break_op (0);
    203         dr.hr.http_status = 0;
    204         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
    205         break;
    206       }
    207       if (GNUNET_OK !=
    208           TALER_exchange_online_purse_created_verify (
    209             etime,
    210             dr.details.ok.purse_expiration,
    211             &dr.details.ok.purse_value_after_fees,
    212             &dr.details.ok.total_deposited,
    213             &pch->purse_pub,
    214             &dr.details.ok.h_contract_terms,
    215             &dr.details.ok.exchange_pub,
    216             &dr.details.ok.exchange_sig))
    217       {
    218         GNUNET_break_op (0);
    219         dr.hr.http_status = 0;
    220         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
    221         break;
    222       }
    223     }
    224     break;
    225   case MHD_HTTP_BAD_REQUEST:
    226     /* This should never happen, either us or the exchange is buggy
    227        (or API version conflict); just pass JSON reply to the application */
    228     dr.hr.ec = TALER_JSON_get_error_code (j);
    229     break;
    230   case MHD_HTTP_FORBIDDEN:
    231     dr.hr.ec = TALER_JSON_get_error_code (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     /* Nothing really to verify, this should never
    239        happen, we should pass the JSON reply to the application */
    240     break;
    241   case MHD_HTTP_CONFLICT:
    242     dr.hr.ec = TALER_JSON_get_error_code (j);
    243     switch (dr.hr.ec)
    244     {
    245     case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA:
    246       {
    247         struct TALER_CoinSpendPublicKeyP coin_pub;
    248         struct TALER_CoinSpendSignatureP coin_sig;
    249         struct TALER_DenominationHashP h_denom_pub;
    250         struct TALER_AgeCommitmentHashP phac;
    251         bool found = false;
    252 
    253         if (GNUNET_OK !=
    254             TALER_EXCHANGE_check_purse_coin_conflict_ (
    255               &pch->purse_pub,
    256               pch->base_url,
    257               j,
    258               &h_denom_pub,
    259               &phac,
    260               &coin_pub,
    261               &coin_sig))
    262         {
    263           GNUNET_break_op (0);
    264           dr.hr.http_status = 0;
    265           dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    266           break;
    267         }
    268         for (unsigned int i = 0; i<pch->num_deposits; i++)
    269         {
    270           struct Coin *coin = &pch->coins[i];
    271           if (0 != GNUNET_memcmp (&coin_pub,
    272                                   &coin->coin_pub))
    273             continue;
    274           if (0 !=
    275               GNUNET_memcmp (&coin->h_denom_pub,
    276                              &h_denom_pub))
    277           {
    278             found = true;
    279             break;
    280           }
    281           if (0 !=
    282               GNUNET_memcmp (&coin->ahac,
    283                              &phac))
    284           {
    285             found = true;
    286             break;
    287           }
    288           if (0 == GNUNET_memcmp (&coin_sig,
    289                                   &coin->coin_sig))
    290           {
    291             /* identical signature => not a conflict */
    292             continue;
    293           }
    294           found = true;
    295           break;
    296         }
    297         if (! found)
    298         {
    299           GNUNET_break_op (0);
    300           dr.hr.http_status = 0;
    301           dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    302           break;
    303         }
    304         /* meta data conflict is real! */
    305         break;
    306       }
    307     case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
    308       /* Nothing to check anymore here, proof needs to be
    309          checked in the GET /coins/$COIN_PUB handler */
    310       break;
    311     case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
    312       break;
    313     default:
    314       GNUNET_break_op (0);
    315       dr.hr.http_status = 0;
    316       dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    317       break;
    318     } /* ec switch */
    319     break;
    320   case MHD_HTTP_GONE:
    321     /* could happen if denomination was revoked or purse expired */
    322     /* Note: one might want to check /keys for revocation
    323        signature here, alas tricky in case our /keys
    324        is outdated => left to clients */
    325     dr.hr.ec = TALER_JSON_get_error_code (j);
    326     break;
    327   case MHD_HTTP_INTERNAL_SERVER_ERROR:
    328     dr.hr.ec = TALER_JSON_get_error_code (j);
    329     /* Server had an internal issue; we should retry, but this API
    330        leaves this to the application */
    331     break;
    332   default:
    333     /* unexpected response code */
    334     dr.hr.ec = TALER_JSON_get_error_code (j);
    335     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    336                 "Unexpected response code %u/%d for exchange deposit\n",
    337                 (unsigned int) response_code,
    338                 dr.hr.ec);
    339     GNUNET_break_op (0);
    340     break;
    341   }
    342   if (TALER_EC_NONE == dr.hr.ec)
    343     dr.hr.hint = NULL;
    344   else
    345     dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec);
    346   pch->cb (pch->cb_cls,
    347            &dr);
    348   TALER_EXCHANGE_post_purses_deposit_cancel (pch);
    349 }
    350 
    351 
    352 struct TALER_EXCHANGE_PostPursesDepositHandle *
    353 TALER_EXCHANGE_post_purses_deposit_create (
    354   struct GNUNET_CURL_Context *ctx,
    355   const char *url,
    356   struct TALER_EXCHANGE_Keys *keys,
    357   const char *purse_exchange_url,
    358   const struct TALER_PurseContractPublicKeyP *purse_pub,
    359   uint8_t min_age,
    360   unsigned int num_deposits,
    361   const struct TALER_EXCHANGE_PurseDeposit deposits[static num_deposits])
    362 {
    363   struct TALER_EXCHANGE_PostPursesDepositHandle *pch;
    364   json_t *deposit_arr;
    365 
    366   // FIXME: use purse_exchange_url for wad transfers (#7271)
    367   (void) purse_exchange_url;
    368   if (0 == num_deposits)
    369   {
    370     GNUNET_break (0);
    371     return NULL;
    372   }
    373   pch = GNUNET_new (struct TALER_EXCHANGE_PostPursesDepositHandle);
    374   pch->ctx = ctx;
    375   pch->base_url = GNUNET_strdup (url);
    376   pch->keys = TALER_EXCHANGE_keys_incref (keys);
    377   pch->purse_pub = *purse_pub;
    378   pch->num_deposits = num_deposits;
    379   pch->coins = GNUNET_new_array (num_deposits,
    380                                  struct Coin);
    381   // FIXME: move JSON construction into _start() function.
    382   deposit_arr = json_array ();
    383   GNUNET_assert (NULL != deposit_arr);
    384   for (unsigned int i = 0; i<num_deposits; i++)
    385   {
    386     const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i];
    387     const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof;
    388     struct Coin *coin = &pch->coins[i];
    389     json_t *jdeposit;
    390     struct TALER_AgeCommitmentHashP *achp = NULL;
    391     struct TALER_AgeAttestationP attest;
    392     struct TALER_AgeAttestationP *attestp = NULL;
    393 
    394     if (NULL != acp)
    395     {
    396       TALER_age_commitment_hash (&acp->commitment,
    397                                  &coin->ahac);
    398       achp = &coin->ahac;
    399       if (GNUNET_OK !=
    400           TALER_age_commitment_attest (acp,
    401                                        min_age,
    402                                        &attest))
    403       {
    404         GNUNET_break (0);
    405         json_decref (deposit_arr);
    406         GNUNET_free (pch->base_url);
    407         GNUNET_free (pch->coins);
    408         TALER_EXCHANGE_keys_decref (pch->keys);
    409         GNUNET_free (pch);
    410         return NULL;
    411       }
    412       attestp = &attest;
    413     }
    414     GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv,
    415                                         &coin->coin_pub.eddsa_pub);
    416     coin->h_denom_pub = deposit->h_denom_pub;
    417     coin->contribution = deposit->amount;
    418     TALER_wallet_purse_deposit_sign (
    419       pch->base_url,
    420       &pch->purse_pub,
    421       &deposit->amount,
    422       &coin->h_denom_pub,
    423       &coin->ahac,
    424       &deposit->coin_priv,
    425       &coin->coin_sig);
    426     jdeposit = GNUNET_JSON_PACK (
    427       GNUNET_JSON_pack_allow_null (
    428         GNUNET_JSON_pack_data_auto ("h_age_commitment",
    429                                     achp)),
    430       GNUNET_JSON_pack_allow_null (
    431         GNUNET_JSON_pack_data_auto ("age_attestation",
    432                                     attestp)),
    433       TALER_JSON_pack_amount ("amount",
    434                               &deposit->amount),
    435       GNUNET_JSON_pack_data_auto ("denom_pub_hash",
    436                                   &deposit->h_denom_pub),
    437       TALER_JSON_pack_denom_sig ("ub_sig",
    438                                  &deposit->denom_sig),
    439       GNUNET_JSON_pack_data_auto ("coin_pub",
    440                                   &coin->coin_pub),
    441       GNUNET_JSON_pack_data_auto ("coin_sig",
    442                                   &coin->coin_sig));
    443     GNUNET_assert (0 ==
    444                    json_array_append_new (deposit_arr,
    445                                           jdeposit));
    446   }
    447   pch->body = GNUNET_JSON_PACK (
    448     GNUNET_JSON_pack_array_steal ("deposits",
    449                                   deposit_arr));
    450   GNUNET_assert (NULL != pch->body);
    451   return pch;
    452 }
    453 
    454 
    455 enum TALER_ErrorCode
    456 TALER_EXCHANGE_post_purses_deposit_start (
    457   struct TALER_EXCHANGE_PostPursesDepositHandle *pch,
    458   TALER_EXCHANGE_PostPursesDepositCallback cb,
    459   TALER_EXCHANGE_POST_PURSES_DEPOSIT_RESULT_CLOSURE *cb_cls)
    460 {
    461   CURL *eh;
    462   char arg_str[sizeof (pch->purse_pub) * 2 + 32];
    463 
    464   pch->cb = cb;
    465   pch->cb_cls = cb_cls;
    466   {
    467     char pub_str[sizeof (pch->purse_pub) * 2];
    468     char *end;
    469 
    470     end = GNUNET_STRINGS_data_to_string (
    471       &pch->purse_pub,
    472       sizeof (pch->purse_pub),
    473       pub_str,
    474       sizeof (pub_str));
    475     *end = '\0';
    476     GNUNET_snprintf (arg_str,
    477                      sizeof (arg_str),
    478                      "purses/%s/deposit",
    479                      pub_str);
    480   }
    481   pch->url = TALER_url_join (pch->base_url,
    482                              arg_str,
    483                              NULL);
    484   if (NULL == pch->url)
    485   {
    486     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    487                 "Could not construct request URL.\n");
    488     return TALER_EC_GENERIC_CONFIGURATION_INVALID;
    489   }
    490   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    491               "URL for purse deposit: `%s'\n",
    492               pch->url);
    493   eh = TALER_EXCHANGE_curl_easy_get_ (pch->url);
    494   if ( (NULL == eh) ||
    495        (GNUNET_OK !=
    496         TALER_curl_easy_post (&pch->post_ctx,
    497                               eh,
    498                               pch->body)) )
    499   {
    500     GNUNET_break (0);
    501     if (NULL != eh)
    502       curl_easy_cleanup (eh);
    503     return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    504   }
    505   pch->job = GNUNET_CURL_job_add2 (pch->ctx,
    506                                    eh,
    507                                    pch->post_ctx.headers,
    508                                    &handle_purse_deposit_finished,
    509                                    pch);
    510   if (NULL == pch->job)
    511     return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    512   return TALER_EC_NONE;
    513 }
    514 
    515 
    516 void
    517 TALER_EXCHANGE_post_purses_deposit_cancel (
    518   struct TALER_EXCHANGE_PostPursesDepositHandle *pch)
    519 {
    520   if (NULL != pch->job)
    521   {
    522     GNUNET_CURL_job_cancel (pch->job);
    523     pch->job = NULL;
    524   }
    525   TALER_curl_easy_post_finished (&pch->post_ctx);
    526   GNUNET_free (pch->base_url);
    527   GNUNET_free (pch->url);
    528   GNUNET_free (pch->coins);
    529   json_decref (pch->body);
    530   TALER_EXCHANGE_keys_decref (pch->keys);
    531   GNUNET_free (pch);
    532 }
    533 
    534 
    535 /* end of exchange_api_post-purses-PURSE_PUB-deposit.c */