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 */