LCOV - code coverage report
Current view: top level - lib - exchange_api_purse_deposit.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 94 196 48.0 %
Date: 2025-06-05 21:03:14 Functions: 3 3 100.0 %

          Line data    Source code
       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 "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_json_lib.h"
      30             : #include "taler_exchange_service.h"
      31             : #include "exchange_api_common.h"
      32             : #include "exchange_api_handle.h"
      33             : #include "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_AgeCommitmentHash 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           7 : handle_purse_deposit_finished (void *cls,
     138             :                                long response_code,
     139             :                                const void *response)
     140             : {
     141           7 :   struct TALER_EXCHANGE_PurseDepositHandle *pch = cls;
     142           7 :   const json_t *j = response;
     143           7 :   struct TALER_EXCHANGE_PurseDepositResponse dr = {
     144             :     .hr.reply = j,
     145           7 :     .hr.http_status = (unsigned int) response_code
     146             :   };
     147           7 :   const struct TALER_EXCHANGE_Keys *keys = pch->keys;
     148             : 
     149           7 :   pch->job = NULL;
     150           7 :   switch (response_code)
     151             :   {
     152           0 :   case 0:
     153           0 :     dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
     154           0 :     break;
     155           5 :   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           5 :         GNUNET_JSON_spec_fixed_auto ("exchange_sig",
     162             :                                      &exchange_sig),
     163           5 :         GNUNET_JSON_spec_fixed_auto ("exchange_pub",
     164             :                                      &exchange_pub),
     165           5 :         GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
     166             :                                      &dr.details.ok.h_contract_terms),
     167           5 :         GNUNET_JSON_spec_timestamp ("exchange_timestamp",
     168             :                                     &etime),
     169           5 :         GNUNET_JSON_spec_timestamp ("purse_expiration",
     170             :                                     &dr.details.ok.purse_expiration),
     171           5 :         TALER_JSON_spec_amount ("total_deposited",
     172           5 :                                 keys->currency,
     173             :                                 &dr.details.ok.total_deposited),
     174           5 :         TALER_JSON_spec_amount ("purse_value_after_fees",
     175           5 :                                 keys->currency,
     176             :                                 &dr.details.ok.purse_value_after_fees),
     177           5 :         GNUNET_JSON_spec_end ()
     178             :       };
     179             : 
     180           5 :       if (GNUNET_OK !=
     181           5 :           GNUNET_JSON_parse (j,
     182             :                              spec,
     183             :                              NULL, NULL))
     184             :       {
     185           0 :         GNUNET_break_op (0);
     186           0 :         dr.hr.http_status = 0;
     187           0 :         dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     188           0 :         break;
     189             :       }
     190           5 :       if (GNUNET_OK !=
     191           5 :           TALER_EXCHANGE_test_signing_key (keys,
     192             :                                            &exchange_pub))
     193             :       {
     194           0 :         GNUNET_break_op (0);
     195           0 :         dr.hr.http_status = 0;
     196           0 :         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
     197           0 :         break;
     198             :       }
     199           5 :       if (GNUNET_OK !=
     200           5 :           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           5 :             &pch->purse_pub,
     206             :             &dr.details.ok.h_contract_terms,
     207             :             &exchange_pub,
     208             :             &exchange_sig))
     209             :       {
     210           0 :         GNUNET_break_op (0);
     211           0 :         dr.hr.http_status = 0;
     212           0 :         dr.hr.ec = TALER_EC_EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID;
     213           0 :         break;
     214             :       }
     215             :     }
     216           5 :     break;
     217           0 :   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           0 :     dr.hr.ec = TALER_JSON_get_error_code (j);
     221           0 :     break;
     222           0 :   case MHD_HTTP_FORBIDDEN:
     223           0 :     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           0 :     break;
     228           0 :   case MHD_HTTP_NOT_FOUND:
     229           0 :     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           0 :     break;
     233           2 :   case MHD_HTTP_CONFLICT:
     234           2 :     dr.hr.ec = TALER_JSON_get_error_code (j);
     235           2 :     switch (dr.hr.ec)
     236             :     {
     237           0 :     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_AgeCommitmentHash phac;
     243           0 :         bool found = false;
     244             : 
     245           0 :         if (GNUNET_OK !=
     246           0 :             TALER_EXCHANGE_check_purse_coin_conflict_ (
     247           0 :               &pch->purse_pub,
     248           0 :               pch->base_url,
     249             :               j,
     250             :               &h_denom_pub,
     251             :               &phac,
     252             :               &coin_pub,
     253             :               &coin_sig))
     254             :         {
     255           0 :           GNUNET_break_op (0);
     256           0 :           dr.hr.http_status = 0;
     257           0 :           dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     258           0 :           break;
     259             :         }
     260           0 :         for (unsigned int i = 0; i<pch->num_deposits; i++)
     261             :         {
     262           0 :           struct Coin *coin = &pch->coins[i];
     263           0 :           if (0 != GNUNET_memcmp (&coin_pub,
     264             :                                   &coin->coin_pub))
     265           0 :             continue;
     266           0 :           if (0 !=
     267           0 :               GNUNET_memcmp (&coin->h_denom_pub,
     268             :                              &h_denom_pub))
     269             :           {
     270           0 :             found = true;
     271           0 :             break;
     272             :           }
     273           0 :           if (0 !=
     274           0 :               GNUNET_memcmp (&coin->ahac,
     275             :                              &phac))
     276             :           {
     277           0 :             found = true;
     278           0 :             break;
     279             :           }
     280           0 :           if (0 == GNUNET_memcmp (&coin_sig,
     281             :                                   &coin->coin_sig))
     282             :           {
     283             :             /* identical signature => not a conflict */
     284           0 :             continue;
     285             :           }
     286           0 :           found = true;
     287           0 :           break;
     288             :         }
     289           0 :         if (! found)
     290             :         {
     291           0 :           GNUNET_break_op (0);
     292           0 :           dr.hr.http_status = 0;
     293           0 :           dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     294           0 :           break;
     295             :         }
     296             :         /* meta data conflict is real! */
     297           0 :         break;
     298             :       }
     299           2 :     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           2 :       break;
     303           0 :     case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
     304           0 :       break;
     305           0 :     default:
     306           0 :       GNUNET_break_op (0);
     307           0 :       dr.hr.http_status = 0;
     308           0 :       dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     309           0 :       break;
     310             :     } /* ec switch */
     311           2 :     break;
     312           0 :   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           0 :     dr.hr.ec = TALER_JSON_get_error_code (j);
     318           0 :     break;
     319           0 :   case MHD_HTTP_INTERNAL_SERVER_ERROR:
     320           0 :     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           0 :     break;
     324           0 :   default:
     325             :     /* unexpected response code */
     326           0 :     dr.hr.ec = TALER_JSON_get_error_code (j);
     327           0 :     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           0 :     GNUNET_break_op (0);
     332           0 :     break;
     333             :   }
     334           7 :   if (TALER_EC_NONE == dr.hr.ec)
     335           5 :     dr.hr.hint = NULL;
     336             :   else
     337           2 :     dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec);
     338           7 :   pch->cb (pch->cb_cls,
     339             :            &dr);
     340           7 :   TALER_EXCHANGE_purse_deposit_cancel (pch);
     341           7 : }
     342             : 
     343             : 
     344             : struct TALER_EXCHANGE_PurseDepositHandle *
     345           7 : 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           7 : {
     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           7 :   if (0 == num_deposits)
     366             :   {
     367           0 :     GNUNET_break (0);
     368           0 :     return NULL;
     369             :   }
     370           7 :   pch = GNUNET_new (struct TALER_EXCHANGE_PurseDepositHandle);
     371           7 :   pch->purse_pub = *purse_pub;
     372           7 :   pch->cb = cb;
     373           7 :   pch->cb_cls = cb_cls;
     374             :   {
     375             :     char pub_str[sizeof (pch->purse_pub) * 2];
     376             :     char *end;
     377             : 
     378           7 :     end = GNUNET_STRINGS_data_to_string (
     379           7 :       &pch->purse_pub,
     380             :       sizeof (pch->purse_pub),
     381             :       pub_str,
     382             :       sizeof (pub_str));
     383           7 :     *end = '\0';
     384           7 :     GNUNET_snprintf (arg_str,
     385             :                      sizeof (arg_str),
     386             :                      "purses/%s/deposit",
     387             :                      pub_str);
     388             :   }
     389           7 :   pch->url = TALER_url_join (url,
     390             :                              arg_str,
     391             :                              NULL);
     392           7 :   if (NULL == pch->url)
     393             :   {
     394           0 :     GNUNET_break (0);
     395           0 :     GNUNET_free (pch);
     396           0 :     return NULL;
     397             :   }
     398           7 :   deposit_arr = json_array ();
     399           7 :   GNUNET_assert (NULL != deposit_arr);
     400           7 :   pch->base_url = GNUNET_strdup (url);
     401           7 :   pch->num_deposits = num_deposits;
     402           7 :   pch->coins = GNUNET_new_array (num_deposits,
     403             :                                  struct Coin);
     404          14 :   for (unsigned int i = 0; i<num_deposits; i++)
     405             :   {
     406           7 :     const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i];
     407           7 :     const struct TALER_AgeCommitmentProof *acp = deposit->age_commitment_proof;
     408           7 :     struct Coin *coin = &pch->coins[i];
     409             :     json_t *jdeposit;
     410           7 :     struct TALER_AgeCommitmentHash *achp = NULL;
     411             :     struct TALER_AgeAttestation attest;
     412           7 :     struct TALER_AgeAttestation *attestp = NULL;
     413             : 
     414           7 :     if (NULL != acp)
     415             :     {
     416           0 :       TALER_age_commitment_hash (&acp->commitment,
     417             :                                  &coin->ahac);
     418           0 :       achp = &coin->ahac;
     419           0 :       if (GNUNET_OK !=
     420           0 :           TALER_age_commitment_attest (acp,
     421             :                                        min_age,
     422             :                                        &attest))
     423             :       {
     424           0 :         GNUNET_break (0);
     425           0 :         json_decref (deposit_arr);
     426           0 :         GNUNET_free (pch->base_url);
     427           0 :         GNUNET_free (pch->coins);
     428           0 :         GNUNET_free (pch->url);
     429           0 :         GNUNET_free (pch);
     430           0 :         return NULL;
     431             :       }
     432           0 :       attestp = &attest;
     433             :     }
     434           7 :     GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv,
     435             :                                         &coin->coin_pub.eddsa_pub);
     436           7 :     coin->h_denom_pub = deposit->h_denom_pub;
     437           7 :     coin->contribution = deposit->amount;
     438           7 :     TALER_wallet_purse_deposit_sign (
     439           7 :       pch->base_url,
     440           7 :       &pch->purse_pub,
     441             :       &deposit->amount,
     442           7 :       &coin->h_denom_pub,
     443           7 :       &coin->ahac,
     444             :       &deposit->coin_priv,
     445             :       &coin->coin_sig);
     446           7 :     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           7 :     GNUNET_assert (0 ==
     464             :                    json_array_append_new (deposit_arr,
     465             :                                           jdeposit));
     466             :   }
     467           7 :   create_obj = GNUNET_JSON_PACK (
     468             :     GNUNET_JSON_pack_array_steal ("deposits",
     469             :                                   deposit_arr));
     470           7 :   GNUNET_assert (NULL != create_obj);
     471           7 :   eh = TALER_EXCHANGE_curl_easy_get_ (pch->url);
     472          14 :   if ( (NULL == eh) ||
     473             :        (GNUNET_OK !=
     474           7 :         TALER_curl_easy_post (&pch->ctx,
     475             :                               eh,
     476             :                               create_obj)) )
     477             :   {
     478           0 :     GNUNET_break (0);
     479           0 :     if (NULL != eh)
     480           0 :       curl_easy_cleanup (eh);
     481           0 :     json_decref (create_obj);
     482           0 :     GNUNET_free (pch->base_url);
     483           0 :     GNUNET_free (pch->url);
     484           0 :     GNUNET_free (pch->coins);
     485           0 :     GNUNET_free (pch);
     486           0 :     return NULL;
     487             :   }
     488           7 :   json_decref (create_obj);
     489           7 :   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
     490             :               "URL for purse deposit: `%s'\n",
     491             :               pch->url);
     492           7 :   pch->keys = TALER_EXCHANGE_keys_incref (keys);
     493          14 :   pch->job = GNUNET_CURL_job_add2 (ctx,
     494             :                                    eh,
     495           7 :                                    pch->ctx.headers,
     496             :                                    &handle_purse_deposit_finished,
     497             :                                    pch);
     498           7 :   return pch;
     499             : }
     500             : 
     501             : 
     502             : void
     503           7 : TALER_EXCHANGE_purse_deposit_cancel (
     504             :   struct TALER_EXCHANGE_PurseDepositHandle *pch)
     505             : {
     506           7 :   if (NULL != pch->job)
     507             :   {
     508           0 :     GNUNET_CURL_job_cancel (pch->job);
     509           0 :     pch->job = NULL;
     510             :   }
     511           7 :   GNUNET_free (pch->base_url);
     512           7 :   GNUNET_free (pch->url);
     513           7 :   GNUNET_free (pch->coins);
     514           7 :   TALER_EXCHANGE_keys_decref (pch->keys);
     515           7 :   TALER_curl_easy_post_finished (&pch->ctx);
     516           7 :   GNUNET_free (pch);
     517           7 : }
     518             : 
     519             : 
     520             : /* end of exchange_api_purse_deposit.c */

Generated by: LCOV version 1.16