exchange

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

test_coin_conflict.c (20357B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 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 <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file lib/test_coin_conflict.c
     18  * @brief test the client-side checks of coin conflict proofs
     19  *        (#TALER_EXCHANGE_check_coin_denomination_conflict_() and
     20  *        #TALER_EXCHANGE_check_coin_age_commitment_conflict_())
     21  *        against replies built the way the exchange builds them
     22  * @author Özgür Kesim
     23  */
     24 #include "platform.h"
     25 #include "taler/taler_util.h"
     26 #include "taler/taler_json_lib.h"
     27 #include "taler/taler_exchange_service.h"
     28 #include "exchange_api_common.h"
     29 
     30 
     31 /**
     32  * Age mask of the age-restricted test denomination.
     33  */
     34 static const struct TALER_AgeMask age_mask = {
     35   .bits = 1 | 1 << 8 | 1 << 10 | 1 << 12
     36           | 1 << 14 | 1 << 16 | 1 << 18 | 1 << 21
     37 };
     38 
     39 
     40 /**
     41  * A test denomination.
     42  */
     43 struct Denom
     44 {
     45   struct TALER_DenominationPrivateKey priv;
     46   struct TALER_DenominationPublicKey pub;
     47   struct TALER_DenominationHashP h;
     48 };
     49 
     50 
     51 /**
     52  * A coin issued by a test denomination.
     53  */
     54 struct Coin
     55 {
     56   struct TALER_CoinSpendPublicKeyP pub;
     57   struct TALER_DenominationSignature sig;
     58 };
     59 
     60 
     61 /**
     62  * Create an RSA test denomination.
     63  *
     64  * @param age_restricted whether the denomination has an age mask
     65  * @param[out] d the denomination
     66  */
     67 static void
     68 make_denom (bool age_restricted,
     69             struct Denom *d)
     70 {
     71   GNUNET_assert (GNUNET_OK ==
     72                  TALER_denom_priv_create (&d->priv,
     73                                           &d->pub,
     74                                           GNUNET_CRYPTO_BSA_RSA,
     75                                           1024));
     76   if (age_restricted)
     77     d->pub.age_mask = age_mask;
     78   TALER_denom_pub_hash (&d->pub,
     79                         &d->h);
     80 }
     81 
     82 
     83 /**
     84  * Issue a coin: a fresh key, blinded, signed by @a d and unblinded.
     85  *
     86  * @param d the issuing denomination
     87  * @param ach age commitment hash to bind into the coin, NULL for none
     88  * @param[out] c the coin
     89  */
     90 static void
     91 make_coin (const struct Denom *d,
     92            const struct TALER_AgeCommitmentHashP *ach,
     93            struct Coin *c)
     94 {
     95   struct TALER_PlanchetMasterSecretP ps;
     96   struct TALER_CoinSpendPrivateKeyP coin_priv;
     97   union GNUNET_CRYPTO_BlindingSecretP bks;
     98   const struct TALER_ExchangeBlindingValues *alg_values;
     99   struct TALER_PlanchetDetail pd;
    100   struct TALER_BlindedDenominationSignature blind_sig;
    101   struct TALER_FreshCoin coin;
    102   struct TALER_CoinPubHashP c_hash;
    103 
    104   alg_values = TALER_denom_ewv_rsa_singleton ();
    105   GNUNET_CRYPTO_random_block (&ps,
    106                               sizeof (ps));
    107   TALER_planchet_setup_coin_priv (&ps,
    108                                   alg_values,
    109                                   &coin_priv);
    110   TALER_planchet_blinding_secret_create (&ps,
    111                                          alg_values,
    112                                          &bks);
    113   GNUNET_assert (GNUNET_OK ==
    114                  TALER_planchet_prepare (&d->pub,
    115                                          alg_values,
    116                                          &bks,
    117                                          NULL,
    118                                          &coin_priv,
    119                                          ach,
    120                                          &c_hash,
    121                                          &pd));
    122   GNUNET_assert (GNUNET_OK ==
    123                  TALER_denom_sign_blinded (&blind_sig,
    124                                            &d->priv,
    125                                            false,
    126                                            &pd.blinded_planchet));
    127   TALER_planchet_detail_free (&pd);
    128   GNUNET_assert (GNUNET_OK ==
    129                  TALER_planchet_to_coin (&d->pub,
    130                                          &blind_sig,
    131                                          &bks,
    132                                          &coin_priv,
    133                                          ach,
    134                                          &c_hash,
    135                                          alg_values,
    136                                          &coin));
    137   TALER_blinded_denom_sig_free (&blind_sig);
    138   GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv,
    139                                       &c->pub.eddsa_pub);
    140   c->sig = coin.sig;
    141 }
    142 
    143 
    144 /**
    145  * Build a denomination conflict reply as the exchange does.
    146  *
    147  * @param c the coin
    148  * @param prev the denomination the exchange knows the coin under
    149  * @param prev_ach age commitment hash stored for the coin, NULL for none
    150  * @return the reply body
    151  */
    152 static json_t *
    153 denom_conflict_reply (const struct Coin *c,
    154                       const struct Denom *prev,
    155                       const struct TALER_AgeCommitmentHashP *prev_ach)
    156 {
    157   json_t *j;
    158 
    159   j = GNUNET_JSON_PACK (
    160     TALER_JSON_pack_ec (
    161       TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY),
    162     GNUNET_JSON_pack_data_auto ("coin_pub",
    163                                 &c->pub),
    164     TALER_JSON_pack_denom_pub ("prev_denom_pub",
    165                                &prev->pub),
    166     TALER_JSON_pack_denom_sig ("prev_denom_sig",
    167                                &c->sig),
    168     GNUNET_JSON_pack_allow_null (
    169       GNUNET_JSON_pack_data_auto ("prev_h_age_commitment",
    170                                   prev_ach)));
    171   GNUNET_assert (NULL != j);
    172   return j;
    173 }
    174 
    175 
    176 /**
    177  * Build an age commitment conflict reply as the exchange does.
    178  *
    179  * @param c the coin
    180  * @param d the denomination of the coin
    181  * @param expected age commitment hash stored for the coin, NULL for none
    182  * @return the reply body
    183  */
    184 static json_t *
    185 age_conflict_reply (const struct Coin *c,
    186                     const struct Denom *d,
    187                     const struct TALER_AgeCommitmentHashP *expected)
    188 {
    189   json_t *j;
    190 
    191   j = GNUNET_JSON_PACK (
    192     TALER_JSON_pack_ec (
    193       TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH),
    194     GNUNET_JSON_pack_data_auto ("coin_pub",
    195                                 &c->pub),
    196     GNUNET_JSON_pack_data_auto ("h_denom_pub",
    197                                 &d->h),
    198     GNUNET_JSON_pack_allow_null (
    199       GNUNET_JSON_pack_data_auto ("expected_age_commitment_hash",
    200                                   expected)),
    201     GNUNET_JSON_pack_string ("conflict_detail",
    202                              "test"),
    203     TALER_JSON_pack_denom_sig ("prev_denom_sig",
    204                                &c->sig));
    205   GNUNET_assert (NULL != j);
    206   return j;
    207 }
    208 
    209 
    210 /**
    211  * Run the denomination conflict check on @a j for a client that used
    212  * @a used.
    213  *
    214  * @param keys exchange keys
    215  * @param j reply to check
    216  * @param used denomination hash the client used
    217  * @param[out] verified set to the verified flag of the parsed reply
    218  * @return result of the check, #GNUNET_SYSERR also if parsing failed
    219  */
    220 static enum GNUNET_GenericReturnValue
    221 run_denom_check (const struct TALER_EXCHANGE_Keys *keys,
    222                  json_t *j,
    223                  const struct TALER_DenominationHashP *used,
    224                  bool *verified)
    225 {
    226   struct TALER_EXCHANGE_CoinDenominationConflict cdc;
    227   enum GNUNET_GenericReturnValue ret;
    228 
    229   *verified = false;
    230   if (GNUNET_OK !=
    231       TALER_EXCHANGE_parse_coin_denomination_conflict_ (j,
    232                                                         &cdc))
    233   {
    234     json_decref (j);
    235     return GNUNET_SYSERR;
    236   }
    237   ret = TALER_EXCHANGE_check_coin_denomination_conflict_ (keys,
    238                                                           used,
    239                                                           &cdc);
    240   *verified = cdc.verified;
    241   TALER_EXCHANGE_free_coin_denomination_conflict_ (&cdc);
    242   json_decref (j);
    243   return ret;
    244 }
    245 
    246 
    247 /**
    248  * Run the age commitment conflict check on @a j for a client that
    249  * used @a used_denom and @a used_ach.
    250  *
    251  * @param keys exchange keys
    252  * @param j reply to check
    253  * @param used_denom denomination hash the client used
    254  * @param used_ach age commitment hash the client used, NULL for none
    255  * @param[out] verified set to the verified flag of the parsed reply
    256  * @return result of the check, #GNUNET_SYSERR also if parsing failed
    257  */
    258 static enum GNUNET_GenericReturnValue
    259 run_age_check (const struct TALER_EXCHANGE_Keys *keys,
    260                json_t *j,
    261                const struct TALER_DenominationHashP *used_denom,
    262                const struct TALER_AgeCommitmentHashP *used_ach,
    263                bool *verified)
    264 {
    265   struct TALER_EXCHANGE_CoinAgeCommitmentConflict cac;
    266   enum GNUNET_GenericReturnValue ret;
    267 
    268   *verified = false;
    269   if (GNUNET_OK !=
    270       TALER_EXCHANGE_parse_coin_age_commitment_conflict_ (j,
    271                                                           &cac))
    272   {
    273     json_decref (j);
    274     return GNUNET_SYSERR;
    275   }
    276   ret = TALER_EXCHANGE_check_coin_age_commitment_conflict_ (keys,
    277                                                             used_denom,
    278                                                             used_ach,
    279                                                             &cac);
    280   *verified = cac.verified;
    281   TALER_EXCHANGE_free_coin_age_commitment_conflict_ (&cac);
    282   json_decref (j);
    283   return ret;
    284 }
    285 
    286 
    287 #define CHECK(cond) do {                                        \
    288           if (! (cond))                                         \
    289           {                                                     \
    290             fprintf (stderr,                                    \
    291                      "FAILED: %s at %s:%u\n",                   \
    292                      #cond, __FILE__, __LINE__);                \
    293             return 1;                                           \
    294           }                                                     \
    295 } while (0)
    296 
    297 
    298 /**
    299  * Denomination conflicts: a coin issued by @a plain is claimed under
    300  * @a aged (valid proof), under @a plain itself (no conflict), by an
    301  * exchange we do not know the denomination of (unverifiable), with a
    302  * tampered coin key (bad signature), and the age hash consistency
    303  * check for a coin issued by @a aged.
    304  *
    305  * @param keys keys listing both denominations
    306  * @param keys_aged keys listing only @a aged
    307  * @param plain denomination without age restriction
    308  * @param aged age-restricted denomination
    309  * @return 0 on success
    310  */
    311 static int
    312 test_denomination_conflicts (const struct TALER_EXCHANGE_Keys *keys,
    313                              const struct TALER_EXCHANGE_Keys *keys_aged,
    314                              const struct Denom *plain,
    315                              const struct Denom *aged)
    316 {
    317   struct Coin c;
    318   struct Coin ca;
    319   struct TALER_AgeCommitmentHashP ach;
    320   struct TALER_AgeCommitmentHashP other;
    321   bool verified;
    322   json_t *j;
    323 
    324   GNUNET_CRYPTO_random_block (&ach,
    325                               sizeof (ach));
    326   GNUNET_CRYPTO_random_block (&other,
    327                               sizeof (other));
    328   make_coin (plain,
    329              NULL,
    330              &c);
    331   make_coin (aged,
    332              &ach,
    333              &ca);
    334 
    335   /* valid proof: known under 'plain', client used 'aged' */
    336   CHECK (GNUNET_OK ==
    337          run_denom_check (keys,
    338                           denom_conflict_reply (&c,
    339                                                 plain,
    340                                                 NULL),
    341                           &aged->h,
    342                           &verified));
    343   CHECK (verified);
    344   /* same denomination: not a conflict */
    345   CHECK (GNUNET_SYSERR ==
    346          run_denom_check (keys,
    347                           denom_conflict_reply (&c,
    348                                                 plain,
    349                                                 NULL),
    350                           &plain->h,
    351                           &verified));
    352   /* 'plain' is not in our keys: accepted, unverifiable */
    353   CHECK (GNUNET_NO ==
    354          run_denom_check (keys_aged,
    355                           denom_conflict_reply (&c,
    356                                                 plain,
    357                                                 NULL),
    358                           &aged->h,
    359                           &verified));
    360   CHECK (! verified);
    361   /* tampered coin key: signature does not verify */
    362   j = denom_conflict_reply (&c,
    363                             plain,
    364                             NULL);
    365   GNUNET_assert (0 ==
    366                  json_object_set_new (j,
    367                                       "coin_pub",
    368                                       GNUNET_JSON_from_data_auto (&ca.pub)));
    369   CHECK (GNUNET_SYSERR ==
    370          run_denom_check (keys,
    371                           j,
    372                           &aged->h,
    373                           &verified));
    374   /* age hash claimed for a denomination without age restriction */
    375   CHECK (GNUNET_SYSERR ==
    376          run_denom_check (keys,
    377                           denom_conflict_reply (&c,
    378                                                 plain,
    379                                                 &ach),
    380                           &aged->h,
    381                           &verified));
    382   /* valid proof with age hash: known under 'aged', client used 'plain' */
    383   CHECK (GNUNET_OK ==
    384          run_denom_check (keys,
    385                           denom_conflict_reply (&ca,
    386                                                 aged,
    387                                                 &ach),
    388                           &plain->h,
    389                           &verified));
    390   CHECK (verified);
    391   /* age hash missing for an age-restricted denomination */
    392   CHECK (GNUNET_SYSERR ==
    393          run_denom_check (keys,
    394                           denom_conflict_reply (&ca,
    395                                                 aged,
    396                                                 NULL),
    397                           &plain->h,
    398                           &verified));
    399   /* wrong age hash: signature does not verify */
    400   CHECK (GNUNET_SYSERR ==
    401          run_denom_check (keys,
    402                           denom_conflict_reply (&ca,
    403                                                 aged,
    404                                                 &other),
    405                           &plain->h,
    406                           &verified));
    407   /* malformed: prev_denom_sig missing */
    408   j = denom_conflict_reply (&c,
    409                             plain,
    410                             NULL);
    411   GNUNET_assert (0 ==
    412                  json_object_del (j,
    413                                   "prev_denom_sig"));
    414   CHECK (GNUNET_SYSERR ==
    415          run_denom_check (keys,
    416                           j,
    417                           &aged->h,
    418                           &verified));
    419   TALER_denom_sig_free (&c.sig);
    420   TALER_denom_sig_free (&ca.sig);
    421   return 0;
    422 }
    423 
    424 
    425 /**
    426  * Age commitment conflicts: a coin issued by @a aged with hash X is
    427  * claimed with hash Y (valid), with X (no conflict), without a hash
    428  * (valid), under another denomination (invalid), by an exchange we do
    429  * not know the denomination of (unverifiable), with a wrong stored
    430  * hash (bad signature); and a coin issued by @a plain without a hash
    431  * claimed with one.
    432  *
    433  * @param keys keys listing both denominations
    434  * @param keys_plain keys listing only @a plain
    435  * @param plain denomination without age restriction
    436  * @param aged age-restricted denomination
    437  * @return 0 on success
    438  */
    439 static int
    440 test_age_conflicts (const struct TALER_EXCHANGE_Keys *keys,
    441                     const struct TALER_EXCHANGE_Keys *keys_plain,
    442                     const struct Denom *plain,
    443                     const struct Denom *aged)
    444 {
    445   struct Coin c;
    446   struct Coin ca;
    447   struct TALER_AgeCommitmentHashP x;
    448   struct TALER_AgeCommitmentHashP y;
    449   bool verified;
    450 
    451   GNUNET_CRYPTO_random_block (&x,
    452                               sizeof (x));
    453   GNUNET_CRYPTO_random_block (&y,
    454                               sizeof (y));
    455   make_coin (aged,
    456              &x,
    457              &ca);
    458   make_coin (plain,
    459              NULL,
    460              &c);
    461 
    462   /* valid: stored X, client used Y */
    463   CHECK (GNUNET_OK ==
    464          run_age_check (keys,
    465                         age_conflict_reply (&ca,
    466                                             aged,
    467                                             &x),
    468                         &aged->h,
    469                         &y,
    470                         &verified));
    471   CHECK (verified);
    472   /* same hash: not a conflict */
    473   CHECK (GNUNET_SYSERR ==
    474          run_age_check (keys,
    475                         age_conflict_reply (&ca,
    476                                             aged,
    477                                             &x),
    478                         &aged->h,
    479                         &x,
    480                         &verified));
    481   /* valid: stored X, client used none */
    482   CHECK (GNUNET_OK ==
    483          run_age_check (keys,
    484                         age_conflict_reply (&ca,
    485                                             aged,
    486                                             &x),
    487                         &aged->h,
    488                         NULL,
    489                         &verified));
    490   CHECK (verified);
    491   /* different denomination: that would be a denomination conflict */
    492   CHECK (GNUNET_SYSERR ==
    493          run_age_check (keys,
    494                         age_conflict_reply (&ca,
    495                                             aged,
    496                                             &x),
    497                         &plain->h,
    498                         &y,
    499                         &verified));
    500   /* 'aged' is not in our keys: accepted, unverifiable */
    501   CHECK (GNUNET_NO ==
    502          run_age_check (keys_plain,
    503                         age_conflict_reply (&ca,
    504                                             aged,
    505                                             &x),
    506                         &aged->h,
    507                         &y,
    508                         &verified));
    509   CHECK (! verified);
    510   /* stored hash claimed to be Y: signature does not verify */
    511   CHECK (GNUNET_SYSERR ==
    512          run_age_check (keys,
    513                         age_conflict_reply (&ca,
    514                                             aged,
    515                                             &y),
    516                         &aged->h,
    517                         &x,
    518                         &verified));
    519   /* valid: stored none, client used Y */
    520   CHECK (GNUNET_OK ==
    521          run_age_check (keys,
    522                         age_conflict_reply (&c,
    523                                             plain,
    524                                             NULL),
    525                         &plain->h,
    526                         &y,
    527                         &verified));
    528   CHECK (verified);
    529   /* stored none, client used none: not a conflict */
    530   CHECK (GNUNET_SYSERR ==
    531          run_age_check (keys,
    532                         age_conflict_reply (&c,
    533                                             plain,
    534                                             NULL),
    535                         &plain->h,
    536                         NULL,
    537                         &verified));
    538   TALER_denom_sig_free (&c.sig);
    539   TALER_denom_sig_free (&ca.sig);
    540   return 0;
    541 }
    542 
    543 
    544 int
    545 main (int argc,
    546       const char *const argv[])
    547 {
    548   struct Denom plain;
    549   struct Denom aged;
    550   struct TALER_EXCHANGE_DenomPublicKey dks[2];
    551   struct TALER_EXCHANGE_Keys keys = {
    552     .denom_keys = dks,
    553     .num_denom_keys = 2
    554   };
    555   struct TALER_EXCHANGE_Keys keys_plain = {
    556     .denom_keys = &dks[0],
    557     .num_denom_keys = 1
    558   };
    559   struct TALER_EXCHANGE_Keys keys_aged = {
    560     .denom_keys = &dks[1],
    561     .num_denom_keys = 1
    562   };
    563   int ret;
    564 
    565   (void) argc;
    566   (void) argv;
    567   GNUNET_log_setup ("test-coin-conflict",
    568                     "WARNING",
    569                     NULL);
    570   make_denom (false,
    571               &plain);
    572   make_denom (true,
    573               &aged);
    574   memset (dks,
    575           0,
    576           sizeof (dks));
    577   dks[0].key = plain.pub;
    578   dks[0].h_key = plain.h;
    579   dks[1].key = aged.pub;
    580   dks[1].h_key = aged.h;
    581 
    582   ret = test_denomination_conflicts (&keys,
    583                                      &keys_aged,
    584                                      &plain,
    585                                      &aged);
    586   if (0 == ret)
    587     ret = test_age_conflicts (&keys,
    588                               &keys_plain,
    589                               &plain,
    590                               &aged);
    591   TALER_denom_priv_free (&plain.priv);
    592   TALER_denom_pub_free (&plain.pub);
    593   TALER_denom_priv_free (&aged.priv);
    594   TALER_denom_pub_free (&aged.pub);
    595   return ret;
    596 }
    597 
    598 
    599 /* end of test_coin_conflict.c */