Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2026 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU General Public License as
7 : published by the Free Software Foundation; either version 3, or
8 : (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful, but
11 : WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public
16 : License along with TALER; see the file COPYING. If not, see
17 : <http://www.gnu.org/licenses/>
18 : */
19 : /**
20 : * @file testing/testing_api_cmd_check_keys.c
21 : * @brief command to compare the /keys of successive "get exchange" commands
22 : * @author Christian Grothoff
23 : */
24 : #include "taler/taler_json_lib.h"
25 : #include <gnunet/gnunet_curl_lib.h>
26 : #include "taler/taler_testing_lib.h"
27 :
28 :
29 : /**
30 : * State for a "check keys" CMD.
31 : */
32 : struct CheckKeysState
33 : {
34 : /**
35 : * Label of the "get exchange" command with the earlier /keys.
36 : */
37 : const char *earlier_ref;
38 :
39 : /**
40 : * Label of the "get exchange" command with the later /keys.
41 : */
42 : const char *later_ref;
43 :
44 : /**
45 : * Label of a "get exchange" command that downloaded the full /keys
46 : * after @e later_ref, or NULL.
47 : */
48 : const char *full_ref;
49 : };
50 :
51 :
52 : /**
53 : * /keys of a "get exchange" command and when they were requested.
54 : */
55 : struct KeysSnapshot
56 : {
57 : /**
58 : * Label of the command.
59 : */
60 : const char *label;
61 :
62 : /**
63 : * The keys.
64 : */
65 : const struct TALER_EXCHANGE_Keys *keys;
66 :
67 : /**
68 : * When the request for @e keys was sent.
69 : */
70 : struct GNUNET_TIME_Absolute request_time;
71 :
72 : /**
73 : * When the reply with @e keys was received.
74 : */
75 : struct GNUNET_TIME_Absolute reply_time;
76 : };
77 :
78 :
79 : /**
80 : * Obtain the /keys of the "get exchange" command @a label.
81 : *
82 : * @param is interpreter state
83 : * @param label label of the command
84 : * @param[out] ks set to the keys of the command
85 : * @return #GNUNET_OK on success
86 : */
87 : static enum GNUNET_GenericReturnValue
88 18 : get_snapshot (struct TALER_TESTING_Interpreter *is,
89 : const char *label,
90 : struct KeysSnapshot *ks)
91 : {
92 : const struct TALER_TESTING_Command *cmd;
93 : struct TALER_EXCHANGE_Keys *keys;
94 : const struct GNUNET_TIME_Absolute *request_time;
95 : const struct GNUNET_TIME_Absolute *reply_time;
96 :
97 18 : cmd = TALER_TESTING_interpreter_lookup_command (is,
98 : label);
99 18 : if (NULL == cmd)
100 : {
101 0 : GNUNET_break (0);
102 0 : return GNUNET_SYSERR;
103 : }
104 18 : if ( (GNUNET_OK !=
105 18 : TALER_TESTING_get_trait_keys (cmd,
106 18 : &keys)) ||
107 36 : (NULL == keys) ||
108 : (GNUNET_OK !=
109 18 : TALER_TESTING_get_trait_absolute_time (cmd,
110 : 0,
111 18 : &request_time)) ||
112 : (GNUNET_OK !=
113 18 : TALER_TESTING_get_trait_absolute_time (cmd,
114 : 1,
115 : &reply_time)) )
116 : {
117 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
118 : "Command `%s' did not download /keys\n",
119 : label);
120 0 : return GNUNET_SYSERR;
121 : }
122 18 : ks->label = label;
123 18 : ks->keys = keys;
124 18 : ks->request_time = *request_time;
125 18 : ks->reply_time = *reply_time;
126 18 : return GNUNET_OK;
127 : }
128 :
129 :
130 : /**
131 : * Find the denomination @a dk in @a keys.
132 : *
133 : * @param keys keys to search
134 : * @param dk denomination to find (by its hash)
135 : * @return NULL if @a keys does not contain @a dk
136 : */
137 : static const struct TALER_EXCHANGE_DenomPublicKey *
138 66 : find_denom (const struct TALER_EXCHANGE_Keys *keys,
139 : const struct TALER_EXCHANGE_DenomPublicKey *dk)
140 : {
141 162 : for (unsigned int i = 0; i<keys->num_denom_keys; i++)
142 162 : if (0 ==
143 162 : GNUNET_memcmp (&dk->h_key,
144 : &keys->denom_keys[i].h_key))
145 66 : return &keys->denom_keys[i];
146 0 : return NULL;
147 : }
148 :
149 :
150 : /**
151 : * Check if @a a and @a b are the same amount.
152 : *
153 : * @param a an amount
154 : * @param b another amount
155 : * @return true if they are equal
156 : */
157 : static bool
158 330 : amount_equal (const struct TALER_Amount *a,
159 : const struct TALER_Amount *b)
160 : {
161 : return (GNUNET_YES ==
162 330 : TALER_amount_cmp_currency (a,
163 660 : b)) &&
164 330 : (0 == TALER_amount_cmp (a,
165 : b));
166 : }
167 :
168 :
169 : /**
170 : * Check if @a a and @a b describe the same denomination with the
171 : * same meta data.
172 : *
173 : * @param a a denomination
174 : * @param b another denomination
175 : * @return true if they are the same
176 : */
177 : static bool
178 66 : denom_equal (const struct TALER_EXCHANGE_DenomPublicKey *a,
179 : const struct TALER_EXCHANGE_DenomPublicKey *b)
180 : {
181 66 : return (0 == GNUNET_memcmp (&a->h_key,
182 66 : &b->h_key)) &&
183 66 : (0 == TALER_denom_pub_cmp (&a->key,
184 66 : &b->key)) &&
185 66 : (0 == GNUNET_memcmp (&a->master_sig,
186 66 : &b->master_sig)) &&
187 66 : (GNUNET_TIME_timestamp_cmp (a->valid_from,
188 : ==,
189 66 : b->valid_from)) &&
190 66 : (GNUNET_TIME_timestamp_cmp (a->withdraw_valid_until,
191 : ==,
192 66 : b->withdraw_valid_until)) &&
193 66 : (GNUNET_TIME_timestamp_cmp (a->expire_deposit,
194 : ==,
195 66 : b->expire_deposit)) &&
196 66 : (GNUNET_TIME_timestamp_cmp (a->expire_legal,
197 : ==,
198 66 : b->expire_legal)) &&
199 66 : amount_equal (&a->value,
200 66 : &b->value) &&
201 66 : amount_equal (&a->fees.withdraw,
202 66 : &b->fees.withdraw) &&
203 66 : amount_equal (&a->fees.deposit,
204 66 : &b->fees.deposit) &&
205 66 : amount_equal (&a->fees.refresh,
206 66 : &b->fees.refresh) &&
207 66 : amount_equal (&a->fees.refund,
208 132 : &b->fees.refund) &&
209 66 : (a->revoked == b->revoked);
210 : }
211 :
212 :
213 : /**
214 : * Check that every denomination of @a a that is still valid after
215 : * @a valid_after is also in @a b, with the same meta data.
216 : *
217 : * @param a keys with the denominations to look for
218 : * @param b keys to search
219 : * @param valid_after denominations of @a a that expire before this
220 : * time are not checked
221 : * @return true if all of them were found
222 : */
223 : static bool
224 18 : denoms_included (const struct KeysSnapshot *a,
225 : const struct KeysSnapshot *b,
226 : struct GNUNET_TIME_Absolute valid_after)
227 : {
228 18 : bool ok = true;
229 :
230 86 : for (unsigned int i = 0; i<a->keys->num_denom_keys; i++)
231 : {
232 68 : const struct TALER_EXCHANGE_DenomPublicKey *dk
233 68 : = &a->keys->denom_keys[i];
234 : const struct TALER_EXCHANGE_DenomPublicKey *dkb;
235 :
236 68 : if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time,
237 : <=,
238 : valid_after))
239 2 : continue;
240 66 : dkb = find_denom (b->keys,
241 : dk);
242 66 : if (NULL == dkb)
243 : {
244 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
245 : "Denomination %s (valid until %s) of `%s' missing in `%s'\n",
246 : GNUNET_h2s (&dk->h_key.hash),
247 : GNUNET_TIME_timestamp2s (dk->expire_deposit),
248 : a->label,
249 : b->label);
250 0 : ok = false;
251 0 : continue;
252 : }
253 66 : if (! denom_equal (dk,
254 : dkb))
255 : {
256 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
257 : "Denomination %s differs between `%s' and `%s'\n",
258 : GNUNET_h2s (&dk->h_key.hash),
259 : a->label,
260 : b->label);
261 0 : ok = false;
262 : }
263 : }
264 18 : return ok;
265 : }
266 :
267 :
268 : /**
269 : * Check that @a ks lists no denomination that had expired
270 : * when its /keys were requested.
271 : *
272 : * @param ks keys to check
273 : * @return true if no expired denomination is listed
274 : */
275 : static bool
276 12 : no_expired_denoms (const struct KeysSnapshot *ks)
277 : {
278 12 : bool ok = true;
279 :
280 56 : for (unsigned int i = 0; i<ks->keys->num_denom_keys; i++)
281 : {
282 44 : const struct TALER_EXCHANGE_DenomPublicKey *dk
283 44 : = &ks->keys->denom_keys[i];
284 :
285 44 : if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time,
286 : <,
287 : ks->request_time))
288 : {
289 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
290 : "`%s' lists denomination %s that expired at %s\n",
291 : ks->label,
292 : GNUNET_h2s (&dk->h_key.hash),
293 : GNUNET_TIME_timestamp2s (dk->expire_deposit));
294 0 : ok = false;
295 : }
296 : }
297 12 : return ok;
298 : }
299 :
300 :
301 : /**
302 : * Check that @a ks lists each denomination only once.
303 : *
304 : * @param ks keys to check
305 : * @return true if there are no duplicates
306 : */
307 : static bool
308 12 : no_duplicate_denoms (const struct KeysSnapshot *ks)
309 : {
310 56 : for (unsigned int i = 0; i<ks->keys->num_denom_keys; i++)
311 108 : for (unsigned int j = i + 1; j<ks->keys->num_denom_keys; j++)
312 64 : if (0 ==
313 64 : GNUNET_memcmp (&ks->keys->denom_keys[i].h_key,
314 : &ks->keys->denom_keys[j].h_key))
315 : {
316 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
317 : "`%s' lists denomination %s twice\n",
318 : ks->label,
319 : GNUNET_h2s (&ks->keys->denom_keys[i].h_key.hash));
320 0 : return false;
321 : }
322 12 : return true;
323 : }
324 :
325 :
326 : /**
327 : * Check that @a later is a valid successor of @a earlier: same master
328 : * public key and currency, all denominations of @a earlier that are
329 : * still valid are kept, none that expired, and none twice.
330 : *
331 : * @param earlier earlier /keys
332 : * @param later later /keys
333 : * @return true if @a later is a valid successor of @a earlier
334 : */
335 : static bool
336 6 : check_successor (const struct KeysSnapshot *earlier,
337 : const struct KeysSnapshot *later)
338 : {
339 6 : bool ok = true;
340 :
341 6 : if (0 !=
342 6 : GNUNET_memcmp (&earlier->keys->master_pub,
343 : &later->keys->master_pub))
344 : {
345 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
346 : "Master public key changed between `%s' and `%s'\n",
347 : earlier->label,
348 : later->label);
349 0 : ok = false;
350 : }
351 6 : if (0 != strcmp (earlier->keys->currency,
352 6 : later->keys->currency))
353 : {
354 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
355 : "Currency changed between `%s' and `%s'\n",
356 : earlier->label,
357 : later->label);
358 0 : ok = false;
359 : }
360 : /* Denominations that were still valid when the later reply was
361 : received must have been kept. */
362 6 : if (! denoms_included (earlier,
363 : later,
364 : later->reply_time))
365 0 : ok = false;
366 6 : if (! no_expired_denoms (later))
367 0 : ok = false;
368 6 : if (! no_duplicate_denoms (later))
369 0 : ok = false;
370 6 : return ok;
371 : }
372 :
373 :
374 : /**
375 : * Check that the auditors of @a a are also auditors in @a b, auditing
376 : * at least the same denominations, ignoring denominations that expire
377 : * before @a valid_after.
378 : *
379 : * @param a keys with the auditors to look for
380 : * @param b keys to search
381 : * @param valid_after ignore denominations that expire before this time
382 : * @return true if all of them were found
383 : */
384 : static bool
385 12 : auditors_included (const struct KeysSnapshot *a,
386 : const struct KeysSnapshot *b,
387 : struct GNUNET_TIME_Absolute valid_after)
388 : {
389 12 : for (unsigned int i = 0; i<a->keys->num_auditors; i++)
390 : {
391 0 : const struct TALER_EXCHANGE_AuditorInformation *ai
392 0 : = &a->keys->auditors[i];
393 0 : const struct TALER_EXCHANGE_AuditorInformation *bi = NULL;
394 :
395 0 : for (unsigned int j = 0; j<b->keys->num_auditors; j++)
396 0 : if ( (0 ==
397 0 : GNUNET_memcmp (&ai->auditor_pub,
398 0 : &b->keys->auditors[j].auditor_pub)) &&
399 0 : (0 == strcmp (ai->auditor_url,
400 0 : b->keys->auditors[j].auditor_url)) )
401 0 : bi = &b->keys->auditors[j];
402 0 : if (NULL == bi)
403 : {
404 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
405 : "Auditor %s of `%s' missing in `%s'\n",
406 : ai->auditor_url,
407 : a->label,
408 : b->label);
409 0 : return false;
410 : }
411 0 : for (unsigned int k = 0; k<ai->num_denom_keys; k++)
412 : {
413 0 : const struct TALER_EXCHANGE_DenomPublicKey *dk
414 0 : = &a->keys->denom_keys[ai->denom_keys[k].denom_key_offset];
415 0 : bool found = false;
416 :
417 0 : if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time,
418 : <=,
419 : valid_after))
420 0 : continue;
421 0 : for (unsigned int l = 0; l<bi->num_denom_keys; l++)
422 0 : if (0 ==
423 0 : GNUNET_memcmp (
424 : &dk->h_key,
425 : &b->keys->denom_keys[bi->denom_keys[l].denom_key_offset].h_key))
426 0 : found = true;
427 0 : if (! found)
428 : {
429 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
430 : "Auditor %s does not audit denomination %s in `%s'\n",
431 : ai->auditor_url,
432 : GNUNET_h2s (&dk->h_key.hash),
433 : b->label);
434 0 : return false;
435 : }
436 : }
437 : }
438 12 : return true;
439 : }
440 :
441 :
442 : /**
443 : * Check that the signing keys of @a a are also in @a b.
444 : *
445 : * @param a keys with the signing keys to look for
446 : * @param b keys to search
447 : * @return true if all of them were found
448 : */
449 : static bool
450 12 : signkeys_included (const struct KeysSnapshot *a,
451 : const struct KeysSnapshot *b)
452 : {
453 24 : for (unsigned int i = 0; i<a->keys->num_sign_keys; i++)
454 : {
455 12 : const struct TALER_EXCHANGE_SigningPublicKey *sk
456 12 : = &a->keys->sign_keys[i];
457 12 : bool found = false;
458 :
459 24 : for (unsigned int j = 0; j<b->keys->num_sign_keys; j++)
460 12 : if ( (0 ==
461 12 : GNUNET_memcmp (&sk->key,
462 12 : &b->keys->sign_keys[j].key)) &&
463 : (0 ==
464 12 : GNUNET_memcmp (&sk->master_sig,
465 : &b->keys->sign_keys[j].master_sig)) )
466 12 : found = true;
467 12 : if (! found)
468 : {
469 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
470 : "Signing key %s of `%s' missing in `%s'\n",
471 : TALER_B2S (&sk->key),
472 : a->label,
473 : b->label);
474 0 : return false;
475 : }
476 : }
477 12 : return true;
478 : }
479 :
480 :
481 : /**
482 : * Check that @a later (cherry-picked and merged by the client) equals
483 : * @a full, downloaded in full right after it. Denominations that
484 : * expire between the two downloads may legitimately differ and are
485 : * ignored.
486 : *
487 : * @param later cherry-picked /keys
488 : * @param full full /keys
489 : * @return true if they are the same
490 : */
491 : static bool
492 6 : check_equal (const struct KeysSnapshot *later,
493 : const struct KeysSnapshot *full)
494 : {
495 6 : bool ok = true;
496 :
497 6 : if (0 !=
498 6 : GNUNET_memcmp (&later->keys->master_pub,
499 : &full->keys->master_pub))
500 : {
501 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
502 : "Master public key differs between `%s' and `%s'\n",
503 : later->label,
504 : full->label);
505 0 : ok = false;
506 : }
507 6 : if (0 != strcmp (later->keys->currency,
508 6 : full->keys->currency))
509 : {
510 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
511 : "Currency differs between `%s' and `%s'\n",
512 : later->label,
513 : full->label);
514 0 : ok = false;
515 : }
516 6 : if (! denoms_included (later,
517 : full,
518 : full->reply_time))
519 0 : ok = false;
520 6 : if (! denoms_included (full,
521 : later,
522 : full->reply_time))
523 0 : ok = false;
524 6 : if (! auditors_included (later,
525 : full,
526 : full->reply_time))
527 0 : ok = false;
528 6 : if (! auditors_included (full,
529 : later,
530 : full->reply_time))
531 0 : ok = false;
532 6 : if (! signkeys_included (later,
533 : full))
534 0 : ok = false;
535 6 : if (! signkeys_included (full,
536 : later))
537 0 : ok = false;
538 6 : if (! no_expired_denoms (full))
539 0 : ok = false;
540 6 : if (! no_duplicate_denoms (full))
541 0 : ok = false;
542 6 : return ok;
543 : }
544 :
545 :
546 : /**
547 : * Run the "check keys" command.
548 : *
549 : * @param cls closure.
550 : * @param cmd the command currently being executed.
551 : * @param is the interpreter state.
552 : */
553 : static void
554 6 : check_keys_run (void *cls,
555 : const struct TALER_TESTING_Command *cmd,
556 : struct TALER_TESTING_Interpreter *is)
557 : {
558 6 : struct CheckKeysState *cks = cls;
559 : struct KeysSnapshot earlier;
560 : struct KeysSnapshot later;
561 :
562 : (void) cmd;
563 6 : if ( (GNUNET_OK !=
564 6 : get_snapshot (is,
565 : cks->earlier_ref,
566 6 : &earlier)) ||
567 : (GNUNET_OK !=
568 6 : get_snapshot (is,
569 : cks->later_ref,
570 : &later)) )
571 : {
572 0 : GNUNET_break (0);
573 0 : TALER_TESTING_interpreter_fail (is);
574 0 : return;
575 : }
576 6 : if (! check_successor (&earlier,
577 : &later))
578 : {
579 0 : GNUNET_break (0);
580 0 : TALER_TESTING_interpreter_fail (is);
581 0 : return;
582 : }
583 6 : if (NULL != cks->full_ref)
584 : {
585 : struct KeysSnapshot full;
586 :
587 6 : if (GNUNET_OK !=
588 6 : get_snapshot (is,
589 : cks->full_ref,
590 : &full))
591 : {
592 0 : GNUNET_break (0);
593 0 : TALER_TESTING_interpreter_fail (is);
594 0 : return;
595 : }
596 6 : if (GNUNET_TIME_absolute_cmp (full.request_time,
597 : <,
598 : later.reply_time))
599 : {
600 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
601 : "`%s' must download /keys after `%s'\n",
602 : cks->full_ref,
603 : cks->later_ref);
604 0 : GNUNET_break (0);
605 0 : TALER_TESTING_interpreter_fail (is);
606 0 : return;
607 : }
608 6 : if (! check_equal (&later,
609 : &full))
610 : {
611 0 : GNUNET_break (0);
612 0 : TALER_TESTING_interpreter_fail (is);
613 0 : return;
614 : }
615 : }
616 6 : TALER_TESTING_interpreter_next (is);
617 : }
618 :
619 :
620 : /**
621 : * Cleanup the state.
622 : *
623 : * @param cls closure.
624 : * @param cmd the command which is being cleaned up.
625 : */
626 : static void
627 6 : check_keys_cleanup (void *cls,
628 : const struct TALER_TESTING_Command *cmd)
629 : {
630 6 : struct CheckKeysState *cks = cls;
631 :
632 : (void) cmd;
633 6 : GNUNET_free (cks);
634 6 : }
635 :
636 :
637 : struct TALER_TESTING_Command
638 6 : TALER_TESTING_cmd_check_keys (const char *label,
639 : const char *earlier_ref,
640 : const char *later_ref,
641 : const char *full_ref)
642 : {
643 : struct CheckKeysState *cks;
644 :
645 6 : GNUNET_assert (NULL != earlier_ref);
646 6 : GNUNET_assert (NULL != later_ref);
647 6 : cks = GNUNET_new (struct CheckKeysState);
648 6 : cks->earlier_ref = earlier_ref;
649 6 : cks->later_ref = later_ref;
650 6 : cks->full_ref = full_ref;
651 : {
652 6 : struct TALER_TESTING_Command cmd = {
653 : .cls = cks,
654 : .label = label,
655 : .run = &check_keys_run,
656 : .cleanup = &check_keys_cleanup
657 : };
658 :
659 6 : return cmd;
660 : }
661 : }
662 :
663 :
664 : /* end of testing_api_cmd_check_keys.c */
|