Line data Source code
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 2 : make_denom (bool age_restricted,
69 : struct Denom *d)
70 : {
71 2 : GNUNET_assert (GNUNET_OK ==
72 : TALER_denom_priv_create (&d->priv,
73 : &d->pub,
74 : GNUNET_CRYPTO_BSA_RSA,
75 : 1024));
76 2 : if (age_restricted)
77 1 : d->pub.age_mask = age_mask;
78 2 : TALER_denom_pub_hash (&d->pub,
79 : &d->h);
80 2 : }
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 4 : 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 4 : alg_values = TALER_denom_ewv_rsa_singleton ();
105 4 : GNUNET_CRYPTO_random_block (&ps,
106 : sizeof (ps));
107 4 : TALER_planchet_setup_coin_priv (&ps,
108 : alg_values,
109 : &coin_priv);
110 4 : TALER_planchet_blinding_secret_create (&ps,
111 : alg_values,
112 : &bks);
113 4 : 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 4 : GNUNET_assert (GNUNET_OK ==
123 : TALER_denom_sign_blinded (&blind_sig,
124 : &d->priv,
125 : false,
126 : &pd.blinded_planchet));
127 4 : TALER_planchet_detail_free (&pd);
128 4 : 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 4 : TALER_blinded_denom_sig_free (&blind_sig);
138 4 : GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv,
139 : &c->pub.eddsa_pub);
140 4 : c->sig = coin.sig;
141 4 : }
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 9 : 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 9 : 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 9 : GNUNET_assert (NULL != j);
172 9 : 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 8 : 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 8 : 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 8 : GNUNET_assert (NULL != j);
206 8 : 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 9 : 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 9 : *verified = false;
230 9 : if (GNUNET_OK !=
231 9 : TALER_EXCHANGE_parse_coin_denomination_conflict_ (j,
232 : &cdc))
233 : {
234 1 : json_decref (j);
235 1 : return GNUNET_SYSERR;
236 : }
237 8 : ret = TALER_EXCHANGE_check_coin_denomination_conflict_ (keys,
238 : used,
239 : &cdc);
240 8 : *verified = cdc.verified;
241 8 : TALER_EXCHANGE_free_coin_denomination_conflict_ (&cdc);
242 8 : json_decref (j);
243 8 : 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 8 : 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 8 : *verified = false;
269 8 : if (GNUNET_OK !=
270 8 : TALER_EXCHANGE_parse_coin_age_commitment_conflict_ (j,
271 : &cac))
272 : {
273 0 : json_decref (j);
274 0 : return GNUNET_SYSERR;
275 : }
276 8 : ret = TALER_EXCHANGE_check_coin_age_commitment_conflict_ (keys,
277 : used_denom,
278 : used_ach,
279 : &cac);
280 8 : *verified = cac.verified;
281 8 : TALER_EXCHANGE_free_coin_age_commitment_conflict_ (&cac);
282 8 : json_decref (j);
283 8 : 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 1 : 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 1 : GNUNET_CRYPTO_random_block (&ach,
325 : sizeof (ach));
326 1 : GNUNET_CRYPTO_random_block (&other,
327 : sizeof (other));
328 1 : make_coin (plain,
329 : NULL,
330 : &c);
331 1 : make_coin (aged,
332 : &ach,
333 : &ca);
334 :
335 : /* valid proof: known under 'plain', client used 'aged' */
336 1 : CHECK (GNUNET_OK ==
337 : run_denom_check (keys,
338 : denom_conflict_reply (&c,
339 : plain,
340 : NULL),
341 : &aged->h,
342 : &verified));
343 1 : CHECK (verified);
344 : /* same denomination: not a conflict */
345 1 : 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 1 : CHECK (GNUNET_NO ==
354 : run_denom_check (keys_aged,
355 : denom_conflict_reply (&c,
356 : plain,
357 : NULL),
358 : &aged->h,
359 : &verified));
360 1 : CHECK (! verified);
361 : /* tampered coin key: signature does not verify */
362 1 : j = denom_conflict_reply (&c,
363 : plain,
364 : NULL);
365 1 : GNUNET_assert (0 ==
366 : json_object_set_new (j,
367 : "coin_pub",
368 : GNUNET_JSON_from_data_auto (&ca.pub)));
369 1 : 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 1 : 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 1 : CHECK (GNUNET_OK ==
384 : run_denom_check (keys,
385 : denom_conflict_reply (&ca,
386 : aged,
387 : &ach),
388 : &plain->h,
389 : &verified));
390 1 : CHECK (verified);
391 : /* age hash missing for an age-restricted denomination */
392 1 : 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 1 : 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 1 : j = denom_conflict_reply (&c,
409 : plain,
410 : NULL);
411 1 : GNUNET_assert (0 ==
412 : json_object_del (j,
413 : "prev_denom_sig"));
414 1 : CHECK (GNUNET_SYSERR ==
415 : run_denom_check (keys,
416 : j,
417 : &aged->h,
418 : &verified));
419 1 : TALER_denom_sig_free (&c.sig);
420 1 : TALER_denom_sig_free (&ca.sig);
421 1 : 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 1 : 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 1 : GNUNET_CRYPTO_random_block (&x,
452 : sizeof (x));
453 1 : GNUNET_CRYPTO_random_block (&y,
454 : sizeof (y));
455 1 : make_coin (aged,
456 : &x,
457 : &ca);
458 1 : make_coin (plain,
459 : NULL,
460 : &c);
461 :
462 : /* valid: stored X, client used Y */
463 1 : 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 1 : CHECK (verified);
472 : /* same hash: not a conflict */
473 1 : 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 1 : 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 1 : CHECK (verified);
491 : /* different denomination: that would be a denomination conflict */
492 1 : 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 1 : 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 1 : CHECK (! verified);
510 : /* stored hash claimed to be Y: signature does not verify */
511 1 : 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 1 : 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 1 : CHECK (verified);
529 : /* stored none, client used none: not a conflict */
530 1 : 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 1 : TALER_denom_sig_free (&c.sig);
539 1 : TALER_denom_sig_free (&ca.sig);
540 1 : return 0;
541 : }
542 :
543 :
544 : int
545 1 : 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 1 : struct TALER_EXCHANGE_Keys keys = {
552 : .denom_keys = dks,
553 : .num_denom_keys = 2
554 : };
555 1 : struct TALER_EXCHANGE_Keys keys_plain = {
556 : .denom_keys = &dks[0],
557 : .num_denom_keys = 1
558 : };
559 1 : 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 1 : GNUNET_log_setup ("test-coin-conflict",
568 : "WARNING",
569 : NULL);
570 1 : make_denom (false,
571 : &plain);
572 1 : make_denom (true,
573 : &aged);
574 1 : memset (dks,
575 : 0,
576 : sizeof (dks));
577 1 : dks[0].key = plain.pub;
578 1 : dks[0].h_key = plain.h;
579 1 : dks[1].key = aged.pub;
580 1 : dks[1].h_key = aged.h;
581 :
582 1 : ret = test_denomination_conflicts (&keys,
583 : &keys_aged,
584 : &plain,
585 : &aged);
586 1 : if (0 == ret)
587 1 : ret = test_age_conflicts (&keys,
588 : &keys_plain,
589 : &plain,
590 : &aged);
591 1 : TALER_denom_priv_free (&plain.priv);
592 1 : TALER_denom_pub_free (&plain.pub);
593 1 : TALER_denom_priv_free (&aged.priv);
594 1 : TALER_denom_pub_free (&aged.pub);
595 1 : return ret;
596 : }
597 :
598 :
599 : /* end of test_coin_conflict.c */
|