testing_api_cmd_check_keys.c (18589B)
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 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 cmd = TALER_TESTING_interpreter_lookup_command (is, 98 label); 99 if (NULL == cmd) 100 { 101 GNUNET_break (0); 102 return GNUNET_SYSERR; 103 } 104 if ( (GNUNET_OK != 105 TALER_TESTING_get_trait_keys (cmd, 106 &keys)) || 107 (NULL == keys) || 108 (GNUNET_OK != 109 TALER_TESTING_get_trait_absolute_time (cmd, 110 0, 111 &request_time)) || 112 (GNUNET_OK != 113 TALER_TESTING_get_trait_absolute_time (cmd, 114 1, 115 &reply_time)) ) 116 { 117 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 118 "Command `%s' did not download /keys\n", 119 label); 120 return GNUNET_SYSERR; 121 } 122 ks->label = label; 123 ks->keys = keys; 124 ks->request_time = *request_time; 125 ks->reply_time = *reply_time; 126 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 find_denom (const struct TALER_EXCHANGE_Keys *keys, 139 const struct TALER_EXCHANGE_DenomPublicKey *dk) 140 { 141 for (unsigned int i = 0; i<keys->num_denom_keys; i++) 142 if (0 == 143 GNUNET_memcmp (&dk->h_key, 144 &keys->denom_keys[i].h_key)) 145 return &keys->denom_keys[i]; 146 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 amount_equal (const struct TALER_Amount *a, 159 const struct TALER_Amount *b) 160 { 161 return (GNUNET_YES == 162 TALER_amount_cmp_currency (a, 163 b)) && 164 (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 denom_equal (const struct TALER_EXCHANGE_DenomPublicKey *a, 179 const struct TALER_EXCHANGE_DenomPublicKey *b) 180 { 181 return (0 == GNUNET_memcmp (&a->h_key, 182 &b->h_key)) && 183 (0 == TALER_denom_pub_cmp (&a->key, 184 &b->key)) && 185 (0 == GNUNET_memcmp (&a->master_sig, 186 &b->master_sig)) && 187 (GNUNET_TIME_timestamp_cmp (a->valid_from, 188 ==, 189 b->valid_from)) && 190 (GNUNET_TIME_timestamp_cmp (a->withdraw_valid_until, 191 ==, 192 b->withdraw_valid_until)) && 193 (GNUNET_TIME_timestamp_cmp (a->expire_deposit, 194 ==, 195 b->expire_deposit)) && 196 (GNUNET_TIME_timestamp_cmp (a->expire_legal, 197 ==, 198 b->expire_legal)) && 199 amount_equal (&a->value, 200 &b->value) && 201 amount_equal (&a->fees.withdraw, 202 &b->fees.withdraw) && 203 amount_equal (&a->fees.deposit, 204 &b->fees.deposit) && 205 amount_equal (&a->fees.refresh, 206 &b->fees.refresh) && 207 amount_equal (&a->fees.refund, 208 &b->fees.refund) && 209 (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 denoms_included (const struct KeysSnapshot *a, 225 const struct KeysSnapshot *b, 226 struct GNUNET_TIME_Absolute valid_after) 227 { 228 bool ok = true; 229 230 for (unsigned int i = 0; i<a->keys->num_denom_keys; i++) 231 { 232 const struct TALER_EXCHANGE_DenomPublicKey *dk 233 = &a->keys->denom_keys[i]; 234 const struct TALER_EXCHANGE_DenomPublicKey *dkb; 235 236 if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time, 237 <=, 238 valid_after)) 239 continue; 240 dkb = find_denom (b->keys, 241 dk); 242 if (NULL == dkb) 243 { 244 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 ok = false; 251 continue; 252 } 253 if (! denom_equal (dk, 254 dkb)) 255 { 256 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 ok = false; 262 } 263 } 264 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 no_expired_denoms (const struct KeysSnapshot *ks) 277 { 278 bool ok = true; 279 280 for (unsigned int i = 0; i<ks->keys->num_denom_keys; i++) 281 { 282 const struct TALER_EXCHANGE_DenomPublicKey *dk 283 = &ks->keys->denom_keys[i]; 284 285 if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time, 286 <, 287 ks->request_time)) 288 { 289 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 ok = false; 295 } 296 } 297 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 no_duplicate_denoms (const struct KeysSnapshot *ks) 309 { 310 for (unsigned int i = 0; i<ks->keys->num_denom_keys; i++) 311 for (unsigned int j = i + 1; j<ks->keys->num_denom_keys; j++) 312 if (0 == 313 GNUNET_memcmp (&ks->keys->denom_keys[i].h_key, 314 &ks->keys->denom_keys[j].h_key)) 315 { 316 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 return false; 321 } 322 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 check_successor (const struct KeysSnapshot *earlier, 337 const struct KeysSnapshot *later) 338 { 339 bool ok = true; 340 341 if (0 != 342 GNUNET_memcmp (&earlier->keys->master_pub, 343 &later->keys->master_pub)) 344 { 345 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 346 "Master public key changed between `%s' and `%s'\n", 347 earlier->label, 348 later->label); 349 ok = false; 350 } 351 if (0 != strcmp (earlier->keys->currency, 352 later->keys->currency)) 353 { 354 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 355 "Currency changed between `%s' and `%s'\n", 356 earlier->label, 357 later->label); 358 ok = false; 359 } 360 /* Denominations that were still valid when the later reply was 361 received must have been kept. */ 362 if (! denoms_included (earlier, 363 later, 364 later->reply_time)) 365 ok = false; 366 if (! no_expired_denoms (later)) 367 ok = false; 368 if (! no_duplicate_denoms (later)) 369 ok = false; 370 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 auditors_included (const struct KeysSnapshot *a, 386 const struct KeysSnapshot *b, 387 struct GNUNET_TIME_Absolute valid_after) 388 { 389 for (unsigned int i = 0; i<a->keys->num_auditors; i++) 390 { 391 const struct TALER_EXCHANGE_AuditorInformation *ai 392 = &a->keys->auditors[i]; 393 const struct TALER_EXCHANGE_AuditorInformation *bi = NULL; 394 395 for (unsigned int j = 0; j<b->keys->num_auditors; j++) 396 if ( (0 == 397 GNUNET_memcmp (&ai->auditor_pub, 398 &b->keys->auditors[j].auditor_pub)) && 399 (0 == strcmp (ai->auditor_url, 400 b->keys->auditors[j].auditor_url)) ) 401 bi = &b->keys->auditors[j]; 402 if (NULL == bi) 403 { 404 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 return false; 410 } 411 for (unsigned int k = 0; k<ai->num_denom_keys; k++) 412 { 413 const struct TALER_EXCHANGE_DenomPublicKey *dk 414 = &a->keys->denom_keys[ai->denom_keys[k].denom_key_offset]; 415 bool found = false; 416 417 if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time, 418 <=, 419 valid_after)) 420 continue; 421 for (unsigned int l = 0; l<bi->num_denom_keys; l++) 422 if (0 == 423 GNUNET_memcmp ( 424 &dk->h_key, 425 &b->keys->denom_keys[bi->denom_keys[l].denom_key_offset].h_key)) 426 found = true; 427 if (! found) 428 { 429 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 return false; 435 } 436 } 437 } 438 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 signkeys_included (const struct KeysSnapshot *a, 451 const struct KeysSnapshot *b) 452 { 453 for (unsigned int i = 0; i<a->keys->num_sign_keys; i++) 454 { 455 const struct TALER_EXCHANGE_SigningPublicKey *sk 456 = &a->keys->sign_keys[i]; 457 bool found = false; 458 459 for (unsigned int j = 0; j<b->keys->num_sign_keys; j++) 460 if ( (0 == 461 GNUNET_memcmp (&sk->key, 462 &b->keys->sign_keys[j].key)) && 463 (0 == 464 GNUNET_memcmp (&sk->master_sig, 465 &b->keys->sign_keys[j].master_sig)) ) 466 found = true; 467 if (! found) 468 { 469 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 return false; 475 } 476 } 477 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 check_equal (const struct KeysSnapshot *later, 493 const struct KeysSnapshot *full) 494 { 495 bool ok = true; 496 497 if (0 != 498 GNUNET_memcmp (&later->keys->master_pub, 499 &full->keys->master_pub)) 500 { 501 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 502 "Master public key differs between `%s' and `%s'\n", 503 later->label, 504 full->label); 505 ok = false; 506 } 507 if (0 != strcmp (later->keys->currency, 508 full->keys->currency)) 509 { 510 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 511 "Currency differs between `%s' and `%s'\n", 512 later->label, 513 full->label); 514 ok = false; 515 } 516 if (! denoms_included (later, 517 full, 518 full->reply_time)) 519 ok = false; 520 if (! denoms_included (full, 521 later, 522 full->reply_time)) 523 ok = false; 524 if (! auditors_included (later, 525 full, 526 full->reply_time)) 527 ok = false; 528 if (! auditors_included (full, 529 later, 530 full->reply_time)) 531 ok = false; 532 if (! signkeys_included (later, 533 full)) 534 ok = false; 535 if (! signkeys_included (full, 536 later)) 537 ok = false; 538 if (! no_expired_denoms (full)) 539 ok = false; 540 if (! no_duplicate_denoms (full)) 541 ok = false; 542 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 check_keys_run (void *cls, 555 const struct TALER_TESTING_Command *cmd, 556 struct TALER_TESTING_Interpreter *is) 557 { 558 struct CheckKeysState *cks = cls; 559 struct KeysSnapshot earlier; 560 struct KeysSnapshot later; 561 562 (void) cmd; 563 if ( (GNUNET_OK != 564 get_snapshot (is, 565 cks->earlier_ref, 566 &earlier)) || 567 (GNUNET_OK != 568 get_snapshot (is, 569 cks->later_ref, 570 &later)) ) 571 { 572 GNUNET_break (0); 573 TALER_TESTING_interpreter_fail (is); 574 return; 575 } 576 if (! check_successor (&earlier, 577 &later)) 578 { 579 GNUNET_break (0); 580 TALER_TESTING_interpreter_fail (is); 581 return; 582 } 583 if (NULL != cks->full_ref) 584 { 585 struct KeysSnapshot full; 586 587 if (GNUNET_OK != 588 get_snapshot (is, 589 cks->full_ref, 590 &full)) 591 { 592 GNUNET_break (0); 593 TALER_TESTING_interpreter_fail (is); 594 return; 595 } 596 if (GNUNET_TIME_absolute_cmp (full.request_time, 597 <, 598 later.reply_time)) 599 { 600 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 601 "`%s' must download /keys after `%s'\n", 602 cks->full_ref, 603 cks->later_ref); 604 GNUNET_break (0); 605 TALER_TESTING_interpreter_fail (is); 606 return; 607 } 608 if (! check_equal (&later, 609 &full)) 610 { 611 GNUNET_break (0); 612 TALER_TESTING_interpreter_fail (is); 613 return; 614 } 615 } 616 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 check_keys_cleanup (void *cls, 628 const struct TALER_TESTING_Command *cmd) 629 { 630 struct CheckKeysState *cks = cls; 631 632 (void) cmd; 633 GNUNET_free (cks); 634 } 635 636 637 struct TALER_TESTING_Command 638 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 GNUNET_assert (NULL != earlier_ref); 646 GNUNET_assert (NULL != later_ref); 647 cks = GNUNET_new (struct CheckKeysState); 648 cks->earlier_ref = earlier_ref; 649 cks->later_ref = later_ref; 650 cks->full_ref = full_ref; 651 { 652 struct TALER_TESTING_Command cmd = { 653 .cls = cks, 654 .label = label, 655 .run = &check_keys_run, 656 .cleanup = &check_keys_cleanup 657 }; 658 659 return cmd; 660 } 661 } 662 663 664 /* end of testing_api_cmd_check_keys.c */